Skip to content

Commit fa38012

Browse files
lankydanKatelyn Baker
authored andcommitted
CORDA-2572 - Add peer information to stacktrace of received FlowException (#4998)
When a `UnexpectedFlowEndException` or a `FlowException` is received the peer that the exception was thrown from will be added to the stacktrace. This is due to it being easier to see and a field that developers are much less likely to override. A nullable field `peer` has been added to `FlowException` and `UnexpectedFlowEndException`. This is read later on (when peer info is not available) to append the peer info to the stacktrace.
1 parent 1802e48 commit fa38012

File tree

4 files changed

+83
-4
lines changed

4 files changed

+83
-4
lines changed

core/src/main/kotlin/net/corda/core/flows/FlowException.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package net.corda.core.flows
22

33
import net.corda.core.CordaException
44
import net.corda.core.CordaRuntimeException
5+
import net.corda.core.identity.Party
56

67
// DOCSTART 1
78
/**
@@ -28,6 +29,9 @@ open class FlowException(message: String?, cause: Throwable?, var originalErrorI
2829
constructor(cause: Throwable?) : this(cause?.toString(), cause)
2930
constructor() : this(null, null)
3031

32+
// private field with obscure name to ensure it is not overridden
33+
private var peer: Party? = null
34+
3135
override fun getErrorId(): Long? = originalErrorId
3236
}
3337
// DOCEND 1
@@ -42,5 +46,8 @@ class UnexpectedFlowEndException(message: String, cause: Throwable?, val origina
4246
constructor(message: String, cause: Throwable?) : this(message, cause, null)
4347
constructor(message: String) : this(message, null)
4448

49+
// private field with obscure name to ensure it is not overridden
50+
private var peer: Party? = null
51+
4552
override fun getErrorId(): Long? = originalErrorId
4653
}

node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -159,10 +159,7 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
159159
is FlowContinuation.Resume -> {
160160
return continuation.result
161161
}
162-
is FlowContinuation.Throw -> {
163-
continuation.throwable.fillInStackTrace()
164-
throw continuation.throwable
165-
}
162+
is FlowContinuation.Throw -> throw continuation.throwable.fillInLocalStackTrace()
166163
FlowContinuation.ProcessEvents -> continue@eventLoop
167164
FlowContinuation.Abort -> abortFiber()
168165
}
@@ -173,6 +170,39 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
173170
}
174171
}
175172

173+
private fun Throwable.fillInLocalStackTrace(): Throwable {
174+
fillInStackTrace()
175+
// provide useful information that can be displayed to the user
176+
// reflection use to access private field
177+
when (this) {
178+
is UnexpectedFlowEndException -> {
179+
DeclaredField<Party?>(UnexpectedFlowEndException::class.java, "peer", this).value?.let {
180+
stackTrace = arrayOf(
181+
StackTraceElement(
182+
"Received unexpected counter-flow exception from peer ${it.name}",
183+
"",
184+
"",
185+
-1
186+
)
187+
) + stackTrace
188+
}
189+
}
190+
is FlowException -> {
191+
DeclaredField<Party?>(FlowException::class.java, "peer", this).value?.let {
192+
stackTrace = arrayOf(
193+
StackTraceElement(
194+
"Received counter-flow exception from peer ${it.name}",
195+
"",
196+
"",
197+
-1
198+
)
199+
) + stackTrace
200+
}
201+
}
202+
}
203+
return this
204+
}
205+
176206
/**
177207
* Immediately processes the passed in event. Always called with an open database transaction.
178208
*

node/src/main/kotlin/net/corda/node/services/statemachine/transitions/DeliverSessionMessageTransition.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package net.corda.node.services.statemachine.transitions
22

3+
import net.corda.core.flows.FlowException
34
import net.corda.core.flows.UnexpectedFlowEndException
5+
import net.corda.core.identity.Party
6+
import net.corda.core.internal.DeclaredField
7+
import net.corda.core.internal.declaredField
48
import net.corda.node.services.statemachine.Action
59
import net.corda.node.services.statemachine.ConfirmSessionMessage
610
import net.corda.node.services.statemachine.DataSessionMessage
@@ -120,6 +124,15 @@ class DeliverSessionMessageTransition(
120124

121125
return when (sessionState) {
122126
is SessionState.Initiated -> {
127+
when (exception) {
128+
// reflection used to access private field
129+
is UnexpectedFlowEndException -> DeclaredField<Party?>(
130+
UnexpectedFlowEndException::class.java,
131+
"peer",
132+
exception
133+
).value = sessionState.peerParty
134+
is FlowException -> DeclaredField<Party?>(FlowException::class.java, "peer", exception).value = sessionState.peerParty
135+
}
123136
val checkpoint = currentState.checkpoint
124137
val sessionId = event.sessionMessage.recipientSessionId
125138
val flowError = FlowError(payload.errorId, exception)

node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import net.corda.core.crypto.SecureHash
1111
import net.corda.core.crypto.random63BitValue
1212
import net.corda.core.flows.*
1313
import net.corda.core.identity.Party
14+
import net.corda.core.internal.DeclaredField
1415
import net.corda.core.internal.concurrent.flatMap
1516
import net.corda.core.messaging.MessageRecipients
1617
import net.corda.core.node.services.PartyInfo
@@ -38,13 +39,15 @@ import net.corda.testing.node.internal.*
3839
import org.assertj.core.api.Assertions.assertThat
3940
import org.assertj.core.api.Assertions.assertThatThrownBy
4041
import org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType
42+
import org.assertj.core.api.Condition
4143
import org.junit.After
4244
import org.junit.Before
4345
import org.junit.Test
4446
import rx.Notification
4547
import rx.Observable
4648
import java.time.Instant
4749
import java.util.*
50+
import java.util.function.Predicate
4851
import kotlin.reflect.KClass
4952
import kotlin.test.assertFailsWith
5053

@@ -186,6 +189,7 @@ class FlowFrameworkTests {
186189
.isThrownBy { receivingFiber.resultFuture.getOrThrow() }
187190
.withMessage("Nothing useful")
188191
.withStackTraceContaining(ReceiveFlow::class.java.name) // Make sure the stack trace is that of the receiving flow
192+
.withStackTraceContaining("Received counter-flow exception from peer")
189193
bobNode.database.transaction {
190194
assertThat(bobNode.internals.checkpointStorage.checkpoints()).isEmpty()
191195
}
@@ -208,6 +212,29 @@ class FlowFrameworkTests {
208212
assertThat((lastMessage.payload as ErrorSessionMessage).flowException!!.stackTrace).isEmpty()
209213
}
210214

215+
@Test
216+
fun `sub-class of FlowException can have a peer field without causing serialisation problems`() {
217+
val exception = MyPeerFlowException("Nothing useful", alice)
218+
bobNode.registerCordappFlowFactory(ReceiveFlow::class) {
219+
ExceptionFlow { exception }
220+
}
221+
222+
val receivingFiber = aliceNode.services.startFlow(ReceiveFlow(bob)) as FlowStateMachineImpl
223+
224+
mockNet.runNetwork()
225+
226+
assertThatExceptionOfType(MyPeerFlowException::class.java)
227+
.isThrownBy { receivingFiber.resultFuture.getOrThrow() }
228+
.has(Condition(Predicate<MyPeerFlowException> { it.peer == alice }, "subclassed peer field has original value"))
229+
.has(Condition(Predicate<MyPeerFlowException> {
230+
DeclaredField<Party?>(
231+
FlowException::class.java,
232+
"peer",
233+
it
234+
).value == bob
235+
}, "FlowException's private peer field has value set"))
236+
}
237+
211238
private class ConditionalExceptionFlow(val otherPartySession: FlowSession, val sendPayload: Any) : FlowLogic<Unit>() {
212239
@Suspendable
213240
override fun call() {
@@ -732,6 +759,8 @@ internal class MyFlowException(override val message: String) : FlowException() {
732759
override fun hashCode(): Int = message.hashCode()
733760
}
734761

762+
internal class MyPeerFlowException(override val message: String, val peer: Party) : FlowException()
763+
735764
@InitiatingFlow
736765
internal class SendAndReceiveFlow(private val otherParty: Party, private val payload: Any, private val otherPartySession: FlowSession? = null) : FlowLogic<Any>() {
737766
constructor(otherPartySession: FlowSession, payload: Any) : this(otherPartySession.counterparty, payload, otherPartySession)

0 commit comments

Comments
 (0)