mirror of
https://github.com/corda/corda.git
synced 2025-02-20 17:33:15 +00:00
Cleaned up the flow stack snapshot API
This commit is contained in:
parent
62b26bcd89
commit
3138e2b6de
@ -140,7 +140,7 @@ abstract class FlowLogic<out T> {
|
||||
* network's event horizon time.
|
||||
*/
|
||||
@Suspendable
|
||||
open fun send(otherParty: Party, payload: Any): Unit = stateMachine.send(otherParty, payload, flowUsedForSessions)
|
||||
open fun send(otherParty: Party, payload: Any) = stateMachine.send(otherParty, payload, flowUsedForSessions)
|
||||
|
||||
/**
|
||||
* Invokes the given subflow. This function returns once the subflow completes successfully with the result
|
||||
@ -239,7 +239,7 @@ abstract class FlowLogic<out T> {
|
||||
* Returns a shallow copy of the Quasar stack frames at the time of call to [flowStackSnapshot]. Use this to inspect
|
||||
* what objects would be serialised at the time of call to a suspending action (e.g. send/receive).
|
||||
* Note: This logic is only available during tests and is not meant to be used during the production deployment.
|
||||
* Therefore the default implementationdoes nothing.
|
||||
* Therefore the default implementation does nothing.
|
||||
*/
|
||||
@Suspendable
|
||||
fun flowStackSnapshot(): FlowStackSnapshot? = stateMachine.flowStackSnapshot(this::class.java)
|
||||
@ -256,7 +256,7 @@ abstract class FlowLogic<out T> {
|
||||
* Therefore the default implementation does nothing.
|
||||
*/
|
||||
@Suspendable
|
||||
fun persistFlowStackSnapshot(): Unit = stateMachine.persistFlowStackSnapshot(this::class.java)
|
||||
fun persistFlowStackSnapshot() = stateMachine.persistFlowStackSnapshot(this::class.java)
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
@ -1,69 +1,21 @@
|
||||
package net.corda.core.flows
|
||||
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import java.nio.file.Path
|
||||
import java.util.*
|
||||
|
||||
interface FlowStackSnapshotFactory {
|
||||
private object Holder {
|
||||
val INSTANCE: FlowStackSnapshotFactory
|
||||
|
||||
init {
|
||||
val serviceFactory = ServiceLoader.load(FlowStackSnapshotFactory::class.java).singleOrNull()
|
||||
INSTANCE = serviceFactory ?: FlowStackSnapshotDefaultFactory()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val instance: FlowStackSnapshotFactory by lazy { Holder.INSTANCE }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns flow stack data snapshot extracted from Quasar stack.
|
||||
* It is designed to be used in the debug mode of the flow execution.
|
||||
* Note. This logic is only available during tests and is not meant to be used during the production deployment.
|
||||
* Therefore the default implementation does nothing.
|
||||
*/
|
||||
fun getFlowStackSnapshot(flowClass: Class<*>): FlowStackSnapshot?
|
||||
|
||||
/** Stores flow stack snapshot as a json file. The stored shapshot is only partial and consists
|
||||
* only data (i.e. stack traces and local variables values) relevant to the flow. It does not
|
||||
* persist corda internal data (e.g. FlowStateMachine). Instead it uses [StackFrameDataToken] to indicate
|
||||
* the class of the element on the stack.
|
||||
* The flow stack snapshot is stored in a file located in
|
||||
* {baseDir}/flowStackSnapshots/YYYY-MM-DD/{flowId}/
|
||||
* where baseDir is the node running directory and flowId is the flow unique identifier generated by the platform.
|
||||
* Note. This logic is only available during tests and is not meant to be used during the production deployment.
|
||||
* Therefore the default implementation does nothing.
|
||||
*/
|
||||
fun persistAsJsonFile(flowClass: Class<*>, baseDir: Path, flowId: String): Unit
|
||||
}
|
||||
|
||||
private class FlowStackSnapshotDefaultFactory : FlowStackSnapshotFactory {
|
||||
val log = loggerFor<FlowStackSnapshotDefaultFactory>()
|
||||
|
||||
override fun getFlowStackSnapshot(flowClass: Class<*>): FlowStackSnapshot? {
|
||||
log.warn("Flow stack snapshot are not supposed to be used in a production deployment")
|
||||
return null
|
||||
}
|
||||
|
||||
override fun persistAsJsonFile(flowClass: Class<*>, baseDir: Path, flowId: String) {
|
||||
log.warn("Flow stack snapshot are not supposed to be used in a production deployment")
|
||||
}
|
||||
}
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
* Main data object representing snapshot of the flow stack, extracted from the Quasar stack.
|
||||
*/
|
||||
data class FlowStackSnapshot constructor(
|
||||
val timestamp: Long = System.currentTimeMillis(),
|
||||
val flowClass: Class<*>? = null,
|
||||
val stackFrames: List<Frame> = listOf()
|
||||
data class FlowStackSnapshot(
|
||||
val time: Instant,
|
||||
val flowClass: Class<out FlowLogic<*>>,
|
||||
val stackFrames: List<Frame>
|
||||
) {
|
||||
data class Frame(
|
||||
val stackTraceElement: StackTraceElement? = null, // This should be the call that *pushed* the frame of [objects]
|
||||
val stackObjects: List<Any?> = listOf()
|
||||
)
|
||||
val stackTraceElement: StackTraceElement, // This should be the call that *pushed* the frame of [objects]
|
||||
val stackObjects: List<Any?>
|
||||
) {
|
||||
override fun toString(): String = stackTraceElement.toString()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -3,11 +3,7 @@ package net.corda.core.internal
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.FlowContext
|
||||
import net.corda.core.flows.FlowInitiator
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.FlowStackSnapshot
|
||||
import net.corda.core.flows.StateMachineRunId
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
@ -39,24 +35,11 @@ interface FlowStateMachine<R> {
|
||||
|
||||
fun recordAuditEvent(eventType: String, comment: String, extraAuditData: Map<String, String>): Unit
|
||||
|
||||
/**
|
||||
* Returns a shallow copy of the Quasar stack frames at the time of call to [flowStackSnapshot]. Use this to inspect
|
||||
* what objects would be serialised at the time of call to a suspending action (e.g. send/receive).
|
||||
*/
|
||||
@Suspendable
|
||||
fun flowStackSnapshot(flowClass: Class<*>): FlowStackSnapshot?
|
||||
fun flowStackSnapshot(flowClass: Class<out FlowLogic<*>>): FlowStackSnapshot?
|
||||
|
||||
/**
|
||||
* Persists a shallow copy of the Quasar stack frames at the time of call to [persistFlowStackSnapshot].
|
||||
* Use this to track the monitor evolution of the quasar stack values during the flow execution.
|
||||
* The flow stack snapshot is stored in a file located in {baseDir}/flowStackSnapshots/YYYY-MM-DD/{flowId}/
|
||||
* where baseDir is the node running directory and flowId is the flow unique identifier generated by the platform.
|
||||
*
|
||||
* Note: With respect to the [flowStackSnapshot], the snapshot being persisted by this method is partial,
|
||||
* meaning that only flow relevant traces and local variables are persisted.
|
||||
*/
|
||||
@Suspendable
|
||||
fun persistFlowStackSnapshot(flowClass: Class<*>): Unit
|
||||
fun persistFlowStackSnapshot(flowClass: Class<out FlowLogic<*>>): Unit
|
||||
|
||||
val serviceHub: ServiceHub
|
||||
val logger: Logger
|
||||
|
@ -0,0 +1,40 @@
|
||||
package net.corda.node.services.statemachine
|
||||
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.FlowStackSnapshot
|
||||
import net.corda.core.flows.StateMachineRunId
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import java.nio.file.Path
|
||||
import java.util.*
|
||||
|
||||
interface FlowStackSnapshotFactory {
|
||||
private object Holder {
|
||||
val INSTANCE: FlowStackSnapshotFactory
|
||||
|
||||
init {
|
||||
val serviceFactory = ServiceLoader.load(FlowStackSnapshotFactory::class.java).singleOrNull()
|
||||
INSTANCE = serviceFactory ?: DefaultFlowStackSnapshotFactory
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val instance: FlowStackSnapshotFactory by lazy { Holder.INSTANCE }
|
||||
}
|
||||
|
||||
fun getFlowStackSnapshot(flowClass: Class<out FlowLogic<*>>): FlowStackSnapshot?
|
||||
|
||||
fun persistAsJsonFile(flowClass: Class<out FlowLogic<*>>, baseDir: Path, flowId: StateMachineRunId)
|
||||
|
||||
private object DefaultFlowStackSnapshotFactory : FlowStackSnapshotFactory {
|
||||
private val log = loggerFor<DefaultFlowStackSnapshotFactory>()
|
||||
|
||||
override fun getFlowStackSnapshot(flowClass: Class<out FlowLogic<*>>): FlowStackSnapshot? {
|
||||
log.warn("Flow stack snapshot are not supposed to be used in a production deployment")
|
||||
return null
|
||||
}
|
||||
|
||||
override fun persistAsJsonFile(flowClass: Class<out FlowLogic<*>>, baseDir: Path, flowId: StateMachineRunId) {
|
||||
log.warn("Flow stack snapshot are not supposed to be used in a production deployment")
|
||||
}
|
||||
}
|
||||
}
|
@ -255,14 +255,12 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun flowStackSnapshot(flowClass: Class<*>): FlowStackSnapshot? {
|
||||
val factory = FlowStackSnapshotFactory.instance
|
||||
return factory.getFlowStackSnapshot(flowClass)
|
||||
override fun flowStackSnapshot(flowClass: Class<out FlowLogic<*>>): FlowStackSnapshot? {
|
||||
return FlowStackSnapshotFactory.instance.getFlowStackSnapshot(flowClass)
|
||||
}
|
||||
|
||||
override fun persistFlowStackSnapshot(flowClass: Class<*>): Unit {
|
||||
val factory = FlowStackSnapshotFactory.instance
|
||||
factory.persistAsJsonFile(flowClass, serviceHub.configuration.baseDirectory, id.toString())
|
||||
override fun persistFlowStackSnapshot(flowClass: Class<out FlowLogic<*>>) {
|
||||
FlowStackSnapshotFactory.instance.persistAsJsonFile(flowClass, serviceHub.configuration.baseDirectory, id)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -4,11 +4,7 @@ import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.FlowContext
|
||||
import net.corda.core.flows.FlowInitiator
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.FlowStackSnapshot
|
||||
import net.corda.core.flows.StateMachineRunId
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.FlowStateMachine
|
||||
import net.corda.core.node.ServiceHub
|
||||
@ -93,21 +89,9 @@ class InteractiveShellTest {
|
||||
override val id: StateMachineRunId get() = throw UnsupportedOperationException()
|
||||
override val resultFuture: CordaFuture<Any?> get() = throw UnsupportedOperationException()
|
||||
override val flowInitiator: FlowInitiator get() = throw UnsupportedOperationException()
|
||||
|
||||
override fun checkFlowPermission(permissionName: String, extraAuditData: Map<String, String>) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
override fun recordAuditEvent(eventType: String, comment: String, extraAuditData: Map<String, String>) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
override fun flowStackSnapshot(flowClass: Class<*>): FlowStackSnapshot? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun persistFlowStackSnapshot(flowClass: Class<*>) {
|
||||
// Do nothing
|
||||
}
|
||||
override fun checkFlowPermission(permissionName: String, extraAuditData: Map<String, String>) = Unit
|
||||
override fun recordAuditEvent(eventType: String, comment: String, extraAuditData: Map<String, String>) = Unit
|
||||
override fun flowStackSnapshot(flowClass: Class<out FlowLogic<*>>): FlowStackSnapshot? = null
|
||||
override fun persistFlowStackSnapshot(flowClass: Class<out FlowLogic<*>>) = Unit
|
||||
}
|
||||
}
|
@ -1,21 +1,23 @@
|
||||
package net.corda.testing
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.FlowStackSnapshot
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.core.flows.StateMachineRunId
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.internal.list
|
||||
import net.corda.core.internal.read
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.jackson.JacksonSupport
|
||||
import net.corda.node.services.startFlowPermission
|
||||
import net.corda.nodeapi.User
|
||||
import net.corda.testing.driver.driver
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
import java.io.File
|
||||
import java.nio.file.Path
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.LocalDate
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
@ -110,15 +112,15 @@ object Constants {
|
||||
* No side effect flow that stores the partial snapshot into a file, path to which is passed in the flow constructor.
|
||||
*/
|
||||
@StartableByRPC
|
||||
class PersistingNoSideEffectFlow : FlowLogic<String>() {
|
||||
class PersistingNoSideEffectFlow : FlowLogic<StateMachineRunId>() {
|
||||
|
||||
@Suspendable
|
||||
override fun call(): String {
|
||||
override fun call(): StateMachineRunId {
|
||||
// Using the [Constants] object here is considered by Quasar as a side effect. Thus explicit initialization
|
||||
@Suppress("UNUSED_VARIABLE")
|
||||
val unusedVar = "inCall"
|
||||
persist()
|
||||
return stateMachine.id.toString()
|
||||
return stateMachine.id
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
@ -134,14 +136,14 @@ class PersistingNoSideEffectFlow : FlowLogic<String>() {
|
||||
* Flow with side effects that stores the partial snapshot into a file, path to which is passed in the flow constructor.
|
||||
*/
|
||||
@StartableByRPC
|
||||
class PersistingSideEffectFlow : FlowLogic<String>() {
|
||||
class PersistingSideEffectFlow : FlowLogic<StateMachineRunId>() {
|
||||
|
||||
@Suspendable
|
||||
override fun call(): String {
|
||||
override fun call(): StateMachineRunId {
|
||||
@Suppress("UNUSED_VARIABLE")
|
||||
val unusedVar = Constants.IN_CALL_VALUE
|
||||
persist()
|
||||
return stateMachine.id.toString()
|
||||
return stateMachine.id
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
@ -156,16 +158,16 @@ class PersistingSideEffectFlow : FlowLogic<String>() {
|
||||
* Similar to [PersistingSideEffectFlow] but aims to produce multiple snapshot files.
|
||||
*/
|
||||
@StartableByRPC
|
||||
class MultiplePersistingSideEffectFlow(val persistCallCount: Int) : FlowLogic<String>() {
|
||||
class MultiplePersistingSideEffectFlow(val persistCallCount: Int) : FlowLogic<StateMachineRunId>() {
|
||||
|
||||
@Suspendable
|
||||
override fun call(): String {
|
||||
override fun call(): StateMachineRunId {
|
||||
@Suppress("UNUSED_VARIABLE")
|
||||
val unusedVar = Constants.IN_CALL_VALUE
|
||||
for (i in 1..persistCallCount) {
|
||||
persist()
|
||||
}
|
||||
return stateMachine.id.toString()
|
||||
return stateMachine.id
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
@ -176,14 +178,19 @@ class MultiplePersistingSideEffectFlow(val persistCallCount: Int) : FlowLogic<St
|
||||
}
|
||||
}
|
||||
|
||||
fun readFlowStackSnapshotFromDir(baseDir: Path, flowId: String): FlowStackSnapshot {
|
||||
val snapshotFile = File(baseDir.toFile(), "flowStackSnapshots/${LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE)}/$flowId/flowStackSnapshot.json")
|
||||
return ObjectMapper().readValue(snapshotFile.inputStream(), FlowStackSnapshot::class.java)
|
||||
fun readFlowStackSnapshotFromDir(baseDir: Path, flowId: StateMachineRunId): FlowStackSnapshot {
|
||||
val snapshotFile = flowSnapshotDir(baseDir, flowId) / "flowStackSnapshot.json"
|
||||
return snapshotFile.read {
|
||||
JacksonSupport.createNonRpcMapper().readValue(it, FlowStackSnapshot::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
fun countFilesInDir(baseDir: Path, flowId: String): Int {
|
||||
val flowDir = File(baseDir.toFile(), "flowStackSnapshots/${LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE)}/$flowId/")
|
||||
return flowDir.listFiles().size
|
||||
private fun flowSnapshotDir(baseDir: Path, flowId: StateMachineRunId): Path {
|
||||
return baseDir / "flowStackSnapshots" / LocalDate.now().toString() / flowId.uuid.toString()
|
||||
}
|
||||
|
||||
fun countFilesInDir(baseDir: Path, flowId: StateMachineRunId): Int {
|
||||
return flowSnapshotDir(baseDir, flowId).list { it.count().toInt() }
|
||||
}
|
||||
|
||||
fun assertFrame(expectedMethod: String, expectedEmpty: Boolean, frame: StackSnapshotFrame) {
|
||||
@ -191,11 +198,11 @@ fun assertFrame(expectedMethod: String, expectedEmpty: Boolean, frame: StackSnap
|
||||
assertEquals(expectedEmpty, frame.dataTypes.isEmpty())
|
||||
}
|
||||
|
||||
@Ignore("When running via gradle the Jacoco agent interferes with the quasar instrumentation process and violates tested" +
|
||||
"criteria (specifically: extra objects are introduced to the quasar stack by th Jacoco agent). You can however " +
|
||||
"run these tests via an IDE.")
|
||||
class FlowStackSnapshotTest {
|
||||
|
||||
@Test
|
||||
@Ignore("This test is skipped due to Jacoco agent interference with the quasar instrumentation process. " +
|
||||
"This violates tested criteria (specifically: extra objects are introduced to the quasar stack by th Jacoco agent)")
|
||||
fun `flowStackSnapshot contains full frames when methods with side effects are called`() {
|
||||
driver(startNodesInProcess = true) {
|
||||
val a = startNode(rpcUsers = listOf(User(Constants.USER, Constants.PASSWORD, setOf(startFlowPermission<SideEffectFlow>())))).get()
|
||||
@ -211,8 +218,6 @@ class FlowStackSnapshotTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore("This test is skipped due to Jacoco agent interference with the quasar instrumentation process. " +
|
||||
"This violates tested criteria (specifically extra objects are introduced to the quasar stack by th Jacoco agent)")
|
||||
fun `flowStackSnapshot contains empty frames when methods with no side effects are called`() {
|
||||
driver(startNodesInProcess = true) {
|
||||
val a = startNode(rpcUsers = listOf(User(Constants.USER, Constants.PASSWORD, setOf(startFlowPermission<NoSideEffectFlow>())))).get()
|
||||
@ -228,8 +233,6 @@ class FlowStackSnapshotTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore("This test is skipped due to Jacoco agent interference with the quasar instrumentation process. " +
|
||||
"This violates tested criteria (specifically extra objects are introduced to the quasar stack by th Jacoco agent)")
|
||||
fun `persistFlowStackSnapshot persists empty frames to a file when methods with no side effects are called`() {
|
||||
driver(startNodesInProcess = true) {
|
||||
val a = startNode(rpcUsers = listOf(User(Constants.USER, Constants.PASSWORD, setOf(startFlowPermission<PersistingNoSideEffectFlow>())))).get()
|
||||
@ -247,8 +250,6 @@ class FlowStackSnapshotTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore("This test is skipped due to Jacoco agent interference with the quasar instrumentation process. " +
|
||||
"This violates tested criteria (specifically extra objects are introduced to the quasar stack by th Jacoco agent)")
|
||||
fun `persistFlowStackSnapshot persists multiple snapshots in different files`() {
|
||||
driver(startNodesInProcess = true) {
|
||||
val a = startNode(rpcUsers = listOf(User(Constants.USER, Constants.PASSWORD, setOf(startFlowPermission<MultiplePersistingSideEffectFlow>())))).get()
|
||||
@ -263,8 +264,6 @@ class FlowStackSnapshotTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore("This test is skipped due to Jacoco agent interference with the quasar instrumentation process. " +
|
||||
"This violates tested criteria (specifically extra objects are introduced to the quasar stack by th Jacoco agent)")
|
||||
fun `persistFlowStackSnapshot stack traces are aligned with stack objects`() {
|
||||
driver(startNodesInProcess = true) {
|
||||
val a = startNode(rpcUsers = listOf(User(Constants.USER, Constants.PASSWORD, setOf(startFlowPermission<PersistingSideEffectFlow>())))).get()
|
||||
@ -279,11 +278,11 @@ class FlowStackSnapshotTest {
|
||||
it.stackObjects.forEach {
|
||||
when (it) {
|
||||
Constants.IN_CALL_VALUE -> {
|
||||
assertEquals(PersistingSideEffectFlow::call.name, trace!!.methodName)
|
||||
assertEquals(PersistingSideEffectFlow::call.name, trace.methodName)
|
||||
inCallCount++
|
||||
}
|
||||
Constants.IN_PERSIST_VALUE -> {
|
||||
assertEquals(PersistingSideEffectFlow::persist.name, trace!!.methodName)
|
||||
assertEquals(PersistingSideEffectFlow::persist.name, trace.methodName)
|
||||
inPersistCount++
|
||||
}
|
||||
}
|
||||
|
@ -5,23 +5,25 @@ import co.paralleluniverse.fibers.Instrumented
|
||||
import co.paralleluniverse.fibers.Stack
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import com.fasterxml.jackson.annotation.JsonInclude
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.databind.SerializationFeature
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.FlowStackSnapshot
|
||||
import net.corda.core.flows.FlowStackSnapshot.Frame
|
||||
import net.corda.core.flows.FlowStackSnapshotFactory
|
||||
import net.corda.core.flows.StackFrameDataToken
|
||||
import net.corda.core.flows.StateMachineRunId
|
||||
import net.corda.core.internal.FlowStateMachine
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.internal.write
|
||||
import net.corda.core.serialization.SerializeAsToken
|
||||
import java.io.File
|
||||
import net.corda.jackson.JacksonSupport
|
||||
import net.corda.node.services.statemachine.FlowStackSnapshotFactory
|
||||
import java.nio.file.Path
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
|
||||
class FlowStackSnapshotFactoryImpl : FlowStackSnapshotFactory {
|
||||
@Suspendable
|
||||
override fun getFlowStackSnapshot(flowClass: Class<*>): FlowStackSnapshot? {
|
||||
override fun getFlowStackSnapshot(flowClass: Class<out FlowLogic<*>>): FlowStackSnapshot {
|
||||
var snapshot: FlowStackSnapshot? = null
|
||||
val stackTrace = Fiber.currentFiber().stackTrace
|
||||
Fiber.parkAndSerialize { fiber, _ ->
|
||||
@ -35,19 +37,19 @@ class FlowStackSnapshotFactoryImpl : FlowStackSnapshotFactory {
|
||||
return temporarySnapshot!!
|
||||
}
|
||||
|
||||
override fun persistAsJsonFile(flowClass: Class<*>, baseDir: Path, flowId: String) {
|
||||
override fun persistAsJsonFile(flowClass: Class<out FlowLogic<*>>, baseDir: Path, flowId: StateMachineRunId) {
|
||||
val flowStackSnapshot = getFlowStackSnapshot(flowClass)
|
||||
val mapper = ObjectMapper()
|
||||
mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
|
||||
mapper.enable(SerializationFeature.INDENT_OUTPUT)
|
||||
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL)
|
||||
val mapper = JacksonSupport.createNonRpcMapper().apply {
|
||||
disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
|
||||
setSerializationInclusion(JsonInclude.Include.NON_NULL)
|
||||
}
|
||||
val file = createFile(baseDir, flowId)
|
||||
file.bufferedWriter().use { out ->
|
||||
mapper.writeValue(out, filterOutStackDump(flowStackSnapshot!!))
|
||||
file.write(createDirs = true) {
|
||||
mapper.writeValue(it, filterOutStackDump(flowStackSnapshot))
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractStackSnapshotFromFiber(fiber: Fiber<*>, stackTrace: List<StackTraceElement>, flowClass: Class<*>): FlowStackSnapshot {
|
||||
private fun extractStackSnapshotFromFiber(fiber: Fiber<*>, stackTrace: List<StackTraceElement>, flowClass: Class<out FlowLogic<*>>): FlowStackSnapshot {
|
||||
val stack = getFiberStack(fiber)
|
||||
val objectStack = getObjectStack(stack).toList()
|
||||
val frameOffsets = getFrameOffsets(stack)
|
||||
@ -58,24 +60,25 @@ class FlowStackSnapshotFactoryImpl : FlowStackSnapshotFactory {
|
||||
val relevantStackTrace = removeConstructorStackTraceElements(stackTrace).drop(1)
|
||||
val stackTraceToAnnotation = relevantStackTrace.map {
|
||||
val element = StackTraceElement(it.className, it.methodName, it.fileName, it.lineNumber)
|
||||
element to getInstrumentedAnnotation(element)
|
||||
element to element.instrumentedAnnotation
|
||||
}
|
||||
val frameObjectsIterator = frameObjects.listIterator()
|
||||
val frames = stackTraceToAnnotation.reversed().map { (element, annotation) ->
|
||||
// If annotation is null then the case indicates that this is an entry point - i.e.
|
||||
// the net.corda.node.services.statemachine.FlowStateMachineImpl.run method
|
||||
if (frameObjectsIterator.hasNext() && (annotation == null || !annotation.methodOptimized)) {
|
||||
Frame(element, frameObjectsIterator.next())
|
||||
val stackObjects = if (frameObjectsIterator.hasNext() && (annotation == null || !annotation.methodOptimized)) {
|
||||
frameObjectsIterator.next()
|
||||
} else {
|
||||
Frame(element, listOf())
|
||||
emptyList()
|
||||
}
|
||||
Frame(element, stackObjects)
|
||||
}
|
||||
return FlowStackSnapshot(flowClass = flowClass, stackFrames = frames)
|
||||
return FlowStackSnapshot(Instant.now(), flowClass, frames)
|
||||
}
|
||||
|
||||
private fun getInstrumentedAnnotation(element: StackTraceElement): Instrumented? {
|
||||
Class.forName(element.className).methods.forEach {
|
||||
if (it.name == element.methodName && it.isAnnotationPresent(Instrumented::class.java)) {
|
||||
private val StackTraceElement.instrumentedAnnotation: Instrumented? get() {
|
||||
Class.forName(className).methods.forEach {
|
||||
if (it.name == methodName && it.isAnnotationPresent(Instrumented::class.java)) {
|
||||
return it.getAnnotation(Instrumented::class.java)
|
||||
}
|
||||
}
|
||||
@ -99,10 +102,10 @@ class FlowStackSnapshotFactoryImpl : FlowStackSnapshotFactory {
|
||||
|
||||
private fun filterOutStackDump(flowStackSnapshot: FlowStackSnapshot): FlowStackSnapshot {
|
||||
val framesFilteredByStackTraceElement = flowStackSnapshot.stackFrames.filter {
|
||||
!FlowStateMachine::class.java.isAssignableFrom(Class.forName(it.stackTraceElement!!.className))
|
||||
!FlowStateMachine::class.java.isAssignableFrom(Class.forName(it.stackTraceElement.className))
|
||||
}
|
||||
val framesFilteredByObjects = framesFilteredByStackTraceElement.map {
|
||||
Frame(it.stackTraceElement, it.stackObjects.map {
|
||||
it.copy(stackObjects = it.stackObjects.map {
|
||||
if (it != null && (it is FlowLogic<*> || it is FlowStateMachine<*> || it is Fiber<*> || it is SerializeAsToken)) {
|
||||
StackFrameDataToken(it::class.java.name)
|
||||
} else {
|
||||
@ -110,25 +113,18 @@ class FlowStackSnapshotFactoryImpl : FlowStackSnapshotFactory {
|
||||
}
|
||||
})
|
||||
}
|
||||
return FlowStackSnapshot(flowStackSnapshot.timestamp, flowStackSnapshot.flowClass, framesFilteredByObjects)
|
||||
return flowStackSnapshot.copy(stackFrames = framesFilteredByObjects)
|
||||
}
|
||||
|
||||
private fun createFile(baseDir: Path, flowId: String): File {
|
||||
val file: File
|
||||
val dir = File(baseDir.toFile(), "flowStackSnapshots/${LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE)}/$flowId/")
|
||||
private fun createFile(baseDir: Path, flowId: StateMachineRunId): Path {
|
||||
val dir = baseDir / "flowStackSnapshots" / LocalDate.now().toString() / flowId.uuid.toString()
|
||||
val index = ThreadLocalIndex.currentIndex.get()
|
||||
if (index == 0) {
|
||||
dir.mkdirs()
|
||||
file = File(dir, "flowStackSnapshot.json")
|
||||
} else {
|
||||
file = File(dir, "flowStackSnapshot-$index.json")
|
||||
}
|
||||
val file = if (index == 0) dir / "flowStackSnapshot.json" else dir / "flowStackSnapshot-$index.json"
|
||||
ThreadLocalIndex.currentIndex.set(index + 1)
|
||||
return file
|
||||
}
|
||||
|
||||
private class ThreadLocalIndex private constructor() {
|
||||
|
||||
companion object {
|
||||
val currentIndex = object : ThreadLocal<Int>() {
|
||||
override fun initialValue() = 0
|
||||
|
Loading…
x
Reference in New Issue
Block a user