Added --logging-level cmd line arg to set the logging level, and improved flow logging

This commit is contained in:
Shams Asari 2017-02-13 14:43:27 +00:00
parent 48c65ac5d2
commit 40dde555e7
24 changed files with 285 additions and 260 deletions

View File

@ -6,17 +6,14 @@
<Property name="log-name">node-${hostName}</Property> <Property name="log-name">node-${hostName}</Property>
<Property name="archive">${sys:log-path}/archive</Property> <Property name="archive">${sys:log-path}/archive</Property>
<Property name="consoleLogLevel">error</Property> <Property name="consoleLogLevel">error</Property>
<Property name="defaultLogLevel">info</Property>
</Properties> </Properties>
<ThresholdFilter level="trace"/> <ThresholdFilter level="trace"/>
<Appenders> <Appenders>
<Console name="Console-Appender" target="SYSTEM_OUT"> <Console name="Console-Appender" target="SYSTEM_OUT">
<PatternLayout> <PatternLayout pattern="%highlight{%level{length=1} %d{HH:mm:ss} [%t] %c{2}.%M - %msg%n}{INFO=white,WARN=red,FATAL=bright red}" />
<pattern>
%highlight{%level{length=1} %d{HH:mm:ss} %T %c{1}.%M - %msg%n}{INFO=white,WARN=red,FATAL=bright red blink}
</pattern>>
</PatternLayout>
</Console> </Console>
<!-- Will generate up to 10 log files for a given day. During every rollover it will delete <!-- Will generate up to 10 log files for a given day. During every rollover it will delete
@ -25,7 +22,7 @@
fileName="${sys:log-path}/${log-name}.log" fileName="${sys:log-path}/${log-name}.log"
filePattern="${archive}/${log-name}.%d{yyyy-MM-dd}-%i.log.gz"> filePattern="${archive}/${log-name}.%d{yyyy-MM-dd}-%i.log.gz">
<PatternLayout pattern="[%-5level] %d{ISO8601}{GMT+0} [%t] %c{1} - %msg%n"/> <PatternLayout pattern="[%-5level] %d{ISO8601}{GMT+0} [%t] %c{2}.%M - %msg%n"/>
<Policies> <Policies>
<TimeBasedTriggeringPolicy/> <TimeBasedTriggeringPolicy/>
@ -47,13 +44,9 @@
</Appenders> </Appenders>
<Loggers> <Loggers>
<Root level="info"> <Root level="${sys:defaultLogLevel}">
<AppenderRef ref="Console-Appender" level="${sys:consoleLogLevel}"/> <AppenderRef ref="Console-Appender" level="${sys:consoleLogLevel}"/>
<AppenderRef ref="RollingFile-Appender" level="info"/> <AppenderRef ref="RollingFile-Appender" />
</Root> </Root>
<Logger name="net.corda" level="info" additivity="false">
<AppenderRef ref="Console-Appender" level="${sys:consoleLogLevel}"/>
<AppenderRef ref="RollingFile-Appender"/>
</Logger>
</Loggers> </Loggers>
</Configuration> </Configuration>

View File

@ -1,19 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Configuration status="info"> <Configuration status="info">
<Properties>
<Property name="defaultLogLevel">info</Property>
</Properties>
<Appenders> <Appenders>
<Console name="Console-Appender" target="SYSTEM_OUT"> <Console name="Console-Appender" target="SYSTEM_OUT">
<PatternLayout> <PatternLayout pattern="[%-5level] %d{HH:mm:ss.SSS} [%t] %c{2}.%M - %msg%n" />
<pattern>
[%-5level] %d{HH:mm:ss.SSS} [%t] %c{1}.%M - %msg%n
</pattern>>
</PatternLayout>
</Console> </Console>
</Appenders> </Appenders>
<Loggers> <Loggers>
<Root level="info"> <Root level="info">
<AppenderRef ref="Console-Appender"/> <AppenderRef ref="Console-Appender"/>
</Root> </Root>
<Logger name="net.corda" level="info" additivity="false"> <Logger name="net.corda" level="${sys:defaultLogLevel}" additivity="false">
<AppenderRef ref="Console-Appender"/> <AppenderRef ref="Console-Appender"/>
</Logger> </Logger>
</Loggers> </Loggers>

View File

@ -23,5 +23,5 @@ import java.security.PublicKey
class Party(val name: String, owningKey: CompositeKey) : AnonymousParty(owningKey) { class Party(val name: String, owningKey: CompositeKey) : AnonymousParty(owningKey) {
/** A helper constructor that converts the given [PublicKey] in to a [CompositeKey] with a single node */ /** A helper constructor that converts the given [PublicKey] in to a [CompositeKey] with a single node */
constructor(name: String, owningKey: PublicKey) : this(name, owningKey.composite) constructor(name: String, owningKey: PublicKey) : this(name, owningKey.composite)
override fun toString() = "${owningKey.toBase58String()} (name)" override fun toString() = "${owningKey.toBase58String()} ($name)"
} }

View File

@ -7,6 +7,7 @@ import net.corda.core.node.ServiceHub
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.ProgressTracker
import net.corda.core.utilities.UntrustworthyData import net.corda.core.utilities.UntrustworthyData
import net.corda.core.utilities.debug
import org.slf4j.Logger import org.slf4j.Logger
import rx.Observable import rx.Observable
@ -135,7 +136,9 @@ abstract class FlowLogic<out T> {
if (shareParentSessions) { if (shareParentSessions) {
subLogic.sessionFlow = this subLogic.sessionFlow = this
} }
logger.debug { "Calling subflow: $subLogic" }
val result = subLogic.call() val result = subLogic.call()
logger.debug { "Subflow finished with result $result" }
// It's easy to forget this when writing flows so we just step it to the DONE state when it completes. // It's easy to forget this when writing flows so we just step it to the DONE state when it completes.
subLogic.progressTracker?.currentStep = ProgressTracker.DONE subLogic.progressTracker?.currentStep = ProgressTracker.DONE
return result return result

View File

@ -44,12 +44,16 @@ abstract class BaseTransaction(
if (timestamp != null) check(notary != null) { "If a timestamp is provided, there must be a notary." } if (timestamp != null) check(notary != null) { "If a timestamp is provided, there must be a notary." }
} }
override fun equals(other: Any?) = override fun equals(other: Any?): Boolean {
other is BaseTransaction && if (other === this) return true
notary == other.notary && return other is BaseTransaction &&
mustSign == other.mustSign && notary == other.notary &&
type == other.type && mustSign == other.mustSign &&
timestamp == other.timestamp type == other.type &&
timestamp == other.timestamp
}
override fun hashCode() = Objects.hash(notary, mustSign, type, timestamp) override fun hashCode() = Objects.hash(notary, mustSign, type, timestamp)
override fun toString(): String = "${javaClass.simpleName}(id=$id)"
} }

View File

@ -143,4 +143,6 @@ data class SignedTransaction(val txBits: SerializedBytes<WireTransaction>,
* @return a digital signature of the transaction. * @return a digital signature of the transaction.
*/ */
fun signWithECDSA(keyPair: KeyPair) = keyPair.signWithECDSA(this.id.bytes) fun signWithECDSA(keyPair: KeyPair) = keyPair.signWithECDSA(this.id.bytes)
override fun toString(): String = "${javaClass.simpleName}(id=$id)"
} }

View File

@ -4,7 +4,6 @@ import net.corda.core.contracts.Attachment
import net.corda.core.crypto.Party import net.corda.core.crypto.Party
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.sha256 import net.corda.core.crypto.sha256
import java.io.ByteArrayInputStream
import java.io.InputStream import java.io.InputStream
/** /**
@ -16,18 +15,19 @@ class FetchAttachmentsFlow(requests: Set<SecureHash>,
override fun load(txid: SecureHash): Attachment? = serviceHub.storageService.attachments.openAttachment(txid) override fun load(txid: SecureHash): Attachment? = serviceHub.storageService.attachments.openAttachment(txid)
override fun convert(wire: ByteArray): Attachment { override fun convert(wire: ByteArray): Attachment = ByteArrayAttachment(wire)
return object : Attachment {
override fun open(): InputStream = ByteArrayInputStream(wire)
override val id: SecureHash = wire.sha256()
override fun equals(other: Any?) = (other is Attachment) && other.id == id
override fun hashCode(): Int = id.hashCode()
}
}
override fun maybeWriteToDisk(downloaded: List<Attachment>) { override fun maybeWriteToDisk(downloaded: List<Attachment>) {
for (attachment in downloaded) { for (attachment in downloaded) {
serviceHub.storageService.attachments.importAttachment(attachment.open()) serviceHub.storageService.attachments.importAttachment(attachment.open())
} }
} }
private class ByteArrayAttachment(private val wire : ByteArray) : Attachment {
override val id: SecureHash by lazy { wire.sha256() }
override fun open(): InputStream = wire.inputStream()
override fun equals(other: Any?) = other === this || other is Attachment && other.id == this.id
override fun hashCode(): Int = id.hashCode()
override fun toString(): String = "${javaClass.simpleName}(id=$id)"
}
} }

View File

@ -22,7 +22,7 @@ class ExampleNodeConfTest {
baseDirectory, baseDirectory,
ConfigHelper.loadConfig( ConfigHelper.loadConfig(
baseDirectory = baseDirectory, baseDirectory = baseDirectory,
configFileOverride = Paths.get(configResource.toURI()) configFile = Paths.get(configResource.toURI())
) )
) )
// Force the config fields as they are resolved lazily // Force the config fields as they are resolved lazily

View File

@ -8,14 +8,11 @@ Jetty web server exposes the same interface over HTTP.
Logging Logging
------- -------
In the default configuration logs are stored to the logs subdirectory of the node directory and are rotated from time to time. You can By default the node log files are stored to the ``logs`` subdirectory of the working directory and are rotated from time
have logging printed to the console as well by passing the ``--log-to-console`` command line flag. Corda to time. You can have logging printed to the console as well by passing the ``--log-to-console`` command line flag.
uses the SL4J logging façade which is configured with the log4j2 binding framework to manage its logging, The default logging level is ``INFO`` which can be adjusted by the ``--logging-level`` command line argument. For more
so you can also configure it in more detail by writing a custom log4j2 logging configuration file and passing ``-Dlog4j.configurationFile=my-config-file.xml`` custom logging, the logger settings can be completely overridden with a `Log4j 2 <https://logging.apache.org/log4j/2.x>`_
on the command line as well. The default configuration is copied during the build from ``config/dev/log4j2.xml``, or for the test sourceSet from ``config/test/log4j2.xml``. configuration file assigned to the ``log4j.configurationFile`` system property.
In corda code a logger is typically instantiated via the ``net.corda.core.utilities.loggerFor`` utility method which will create an SL4J ``Logger`` with a name based on the type parameter.
Also, available in ``net.corda.core.utilities``, are extension methods to take a lazily evaluated logging lambda for trace and debug level, which will not evaluate the lambda if the LogLevel threshold is higher.
Database access Database access
--------------- ---------------

View File

@ -11,7 +11,6 @@ import net.corda.core.messaging.StateMachineUpdate
import net.corda.core.messaging.startFlow import net.corda.core.messaging.startFlow
import net.corda.core.node.NodeInfo import net.corda.core.node.NodeInfo
import net.corda.core.serialization.OpaqueBytes import net.corda.core.serialization.OpaqueBytes
import net.corda.core.transactions.SignedTransaction
import net.corda.flows.CashIssueFlow import net.corda.flows.CashIssueFlow
import net.corda.flows.CashPaymentFlow import net.corda.flows.CashPaymentFlow
import net.corda.node.driver.DriverBasedTest import net.corda.node.driver.DriverBasedTest

View File

@ -2,8 +2,10 @@ package net.corda.node
import com.typesafe.config.Config import com.typesafe.config.Config
import joptsimple.OptionParser import joptsimple.OptionParser
import joptsimple.util.EnumConverter
import net.corda.core.div import net.corda.core.div
import net.corda.node.services.config.ConfigHelper import net.corda.node.services.config.ConfigHelper
import org.slf4j.event.Level
import java.io.PrintStream import java.io.PrintStream
import java.nio.file.Path import java.nio.file.Path
import java.nio.file.Paths import java.nio.file.Paths
@ -20,6 +22,11 @@ class ArgsParser {
.accepts("config-file", "The path to the config file") .accepts("config-file", "The path to the config file")
.withRequiredArg() .withRequiredArg()
.defaultsTo("node.conf") .defaultsTo("node.conf")
private val loggerLevel = optionParser
.accepts("logging-level", "Enable logging at this level and higher")
.withRequiredArg()
.withValuesConvertedBy(object : EnumConverter<Level>(Level::class.java) {})
.defaultsTo(Level.INFO)
private val logToConsoleArg = optionParser.accepts("log-to-console", "If set, prints logging to the console as well as to a file.") private val logToConsoleArg = optionParser.accepts("log-to-console", "If set, prints logging to the console as well as to a file.")
private val isWebserverArg = optionParser.accepts("webserver") private val isWebserverArg = optionParser.accepts("webserver")
private val helpArg = optionParser.accepts("help").forHelp() private val helpArg = optionParser.accepts("help").forHelp()
@ -31,14 +38,22 @@ class ArgsParser {
} }
val baseDirectory = Paths.get(optionSet.valueOf(baseDirectoryArg)).normalize().toAbsolutePath() val baseDirectory = Paths.get(optionSet.valueOf(baseDirectoryArg)).normalize().toAbsolutePath()
val configFile = baseDirectory / optionSet.valueOf(configFileArg) val configFile = baseDirectory / optionSet.valueOf(configFileArg)
val help = optionSet.has(helpArg)
val loggingLevel = optionSet.valueOf(loggerLevel)
val logToConsole = optionSet.has(logToConsoleArg)
val isWebserver = optionSet.has(isWebserverArg) val isWebserver = optionSet.has(isWebserverArg)
return CmdLineOptions(baseDirectory, configFile, optionSet.has(helpArg), optionSet.has(logToConsoleArg), isWebserver) return CmdLineOptions(baseDirectory, configFile, help, loggingLevel, logToConsole, isWebserver)
} }
fun printHelp(sink: PrintStream) = optionParser.printHelpOn(sink) fun printHelp(sink: PrintStream) = optionParser.printHelpOn(sink)
} }
data class CmdLineOptions(val baseDirectory: Path, val configFile: Path?, val help: Boolean, val logToConsole: Boolean, val isWebserver: Boolean) { data class CmdLineOptions(val baseDirectory: Path,
val configFile: Path,
val help: Boolean,
val loggingLevel: Level,
val logToConsole: Boolean,
val isWebserver: Boolean) {
fun loadConfig(allowMissingConfig: Boolean = false, configOverrides: Map<String, Any?> = emptyMap()): Config { fun loadConfig(allowMissingConfig: Boolean = false, configOverrides: Map<String, Any?> = emptyMap()): Config {
return ConfigHelper.loadConfig(baseDirectory, configFile, allowMissingConfig, configOverrides) return ConfigHelper.loadConfig(baseDirectory, configFile, allowMissingConfig, configOverrides)
} }

View File

@ -2,6 +2,7 @@
package net.corda.node package net.corda.node
import com.typesafe.config.ConfigException import com.typesafe.config.ConfigException
import joptsimple.OptionException
import net.corda.core.* import net.corda.core.*
import net.corda.core.utilities.Emoji import net.corda.core.utilities.Emoji
import net.corda.node.internal.Node import net.corda.node.internal.Node
@ -14,7 +15,6 @@ import org.slf4j.LoggerFactory
import java.lang.management.ManagementFactory import java.lang.management.ManagementFactory
import java.net.InetAddress import java.net.InetAddress
import java.nio.file.Paths import java.nio.file.Paths
import kotlin.concurrent.thread
import kotlin.system.exitProcess import kotlin.system.exitProcess
private var renderBasicInfoToConsole = true private var renderBasicInfoToConsole = true
@ -38,8 +38,9 @@ fun main(args: Array<String>) {
val cmdlineOptions = try { val cmdlineOptions = try {
argsParser.parse(*args) argsParser.parse(*args)
} catch (ex: Exception) { } catch (ex: OptionException) {
println("Unknown command line arguments: ${ex.message}") println("Invalid command line arguments: ${ex.message}")
argsParser.printHelp(System.out)
exitProcess(1) exitProcess(1)
} }
@ -49,10 +50,10 @@ fun main(args: Array<String>) {
exitProcess(0) exitProcess(0)
} }
// Set up logging. // Set up logging. These properties are referenced from the XML config file.
System.setProperty("defaultLogLevel", cmdlineOptions.loggingLevel.name.toLowerCase())
if (cmdlineOptions.logToConsole) { if (cmdlineOptions.logToConsole) {
// This property is referenced from the XML config file. System.setProperty("consoleLogLevel", cmdlineOptions.loggingLevel.name.toLowerCase())
System.setProperty("consoleLogLevel", "info")
renderBasicInfoToConsole = false renderBasicInfoToConsole = false
} }

View File

@ -147,7 +147,7 @@ sealed class PortAllocation {
* @param portAllocation The port allocation strategy to use for the messaging and the web server addresses. Defaults to incremental. * @param portAllocation The port allocation strategy to use for the messaging and the web server addresses. Defaults to incremental.
* @param debugPortAllocation The port allocation strategy to use for jvm debugging. Defaults to incremental. * @param debugPortAllocation The port allocation strategy to use for jvm debugging. Defaults to incremental.
* @param useTestClock If true the test clock will be used in Node. * @param useTestClock If true the test clock will be used in Node.
* @param isDebug Indicates whether the spawned nodes should start in jdwt debug mode. * @param isDebug Indicates whether the spawned nodes should start in jdwt debug mode and have debug level logging.
* @param dsl The dsl itself. * @param dsl The dsl itself.
* @return The value returned in the [dsl] closure. * @return The value returned in the [dsl] closure.
*/ */
@ -502,19 +502,19 @@ open class DriverDSL(
else else
"" ""
val additionalKeys = listOf("amq.delivery.delay.ms") val inheritedProperties = listOf("amq.delivery.delay.ms")
val systemArgs = mutableMapOf( val systemArgs = mutableMapOf(
"name" to nodeConf.myLegalName, "name" to nodeConf.myLegalName,
"visualvm.display.name" to "Corda" "visualvm.display.name" to "Corda"
) )
for (key in additionalKeys) { inheritedProperties.forEach {
if (System.getProperty(key) != null) { val property = System.getProperty(it)
systemArgs.set(key, System.getProperty(key)) if (property != null) systemArgs[it] = property
}
} }
val loggingLevel = if (debugPort == null) "INFO" else "DEBUG"
val javaArgs = listOf(path) + val javaArgs = listOf(path) +
systemArgs.map { "-D${it.key}=${it.value}" } + systemArgs.map { "-D${it.key}=${it.value}" } +
listOf( listOf(
@ -524,7 +524,8 @@ open class DriverDSL(
"-XX:+UseG1GC", "-XX:+UseG1GC",
"-cp", classpath, "-cp", classpath,
className, className,
"--base-directory=${nodeConf.baseDirectory}" "--base-directory=${nodeConf.baseDirectory}",
"--logging-level=$loggingLevel"
).filter(String::isNotEmpty) ).filter(String::isNotEmpty)
val builder = ProcessBuilder(javaArgs) val builder = ProcessBuilder(javaArgs)
builder.redirectError(Paths.get("error.$className.log").toFile()) builder.redirectError(Paths.get("error.$className.log").toFile())

View File

@ -28,19 +28,16 @@ object ConfigHelper {
private val log = loggerFor<ConfigHelper>() private val log = loggerFor<ConfigHelper>()
fun loadConfig(baseDirectory: Path, fun loadConfig(baseDirectory: Path,
configFileOverride: Path? = null, configFile: Path = baseDirectory / "node.conf",
allowMissingConfig: Boolean = false, allowMissingConfig: Boolean = false,
configOverrides: Map<String, Any?> = emptyMap()): Config { configOverrides: Map<String, Any?> = emptyMap()): Config {
val defaultConfig = ConfigFactory.parseResources("reference.conf", ConfigParseOptions.defaults().setAllowMissing(false)) val parseOptions = ConfigParseOptions.defaults()
val defaultConfig = ConfigFactory.parseResources("reference.conf", parseOptions.setAllowMissing(false))
val configFile = configFileOverride ?: baseDirectory / "node.conf" val appConfig = ConfigFactory.parseFile(configFile.toFile(), parseOptions.setAllowMissing(allowMissingConfig))
val appConfig = ConfigFactory.parseFile(configFile.toFile(), ConfigParseOptions.defaults().setAllowMissing(allowMissingConfig))
val overrideConfig = ConfigFactory.parseMap(configOverrides + mapOf( val overrideConfig = ConfigFactory.parseMap(configOverrides + mapOf(
// Add substitution values here // Add substitution values here
"basedir" to baseDirectory.toString()) "basedir" to baseDirectory.toString())
) )
val finalConfig = overrideConfig val finalConfig = overrideConfig
.withFallback(appConfig) .withFallback(appConfig)
.withFallback(defaultConfig) .withFallback(defaultConfig)

View File

@ -67,7 +67,7 @@ class NodeMessagingClient(override val config: NodeConfiguration,
// confusion. // confusion.
const val TOPIC_PROPERTY = "platform-topic" const val TOPIC_PROPERTY = "platform-topic"
const val SESSION_ID_PROPERTY = "session-id" const val SESSION_ID_PROPERTY = "session-id"
val AMQ_DELAY = Integer.valueOf(System.getProperty("amq.delivery.delay.ms", "0")) private val AMQ_DELAY: Int = Integer.valueOf(System.getProperty("amq.delivery.delay.ms", "0"))
} }
private class InnerState { private class InnerState {
@ -88,12 +88,16 @@ class NodeMessagingClient(override val config: NodeConfiguration,
val callback: (ReceivedMessage, MessageHandlerRegistration) -> Unit) : MessageHandlerRegistration val callback: (ReceivedMessage, MessageHandlerRegistration) -> Unit) : MessageHandlerRegistration
/** An executor for sending messages */ /** An executor for sending messages */
private val messagingExecutor = AffinityExecutor.ServiceAffinityExecutor("${config.myLegalName} Messaging", 1) private val messagingExecutor = AffinityExecutor.ServiceAffinityExecutor("Messaging", 1)
/** /**
* Apart from the NetworkMapService this is the only other address accessible to the node outside of lookups against the NetworkMapCache. * Apart from the NetworkMapService this is the only other address accessible to the node outside of lookups against the NetworkMapCache.
*/ */
override val myAddress: SingleMessageRecipient = if (myIdentity != null) NodeAddress.asPeer(myIdentity, serverHostPort) else NetworkMapAddress(serverHostPort) override val myAddress: SingleMessageRecipient = if (myIdentity != null) {
NodeAddress.asPeer(myIdentity, serverHostPort)
} else {
NetworkMapAddress(serverHostPort)
}
private val state = ThreadBox(InnerState()) private val state = ThreadBox(InnerState())
private val handlers = CopyOnWriteArrayList<Handler>() private val handlers = CopyOnWriteArrayList<Handler>()
@ -108,7 +112,8 @@ class NodeMessagingClient(override val config: NodeConfiguration,
override fun addElementToInsert(insert: InsertStatement, entry: UUID, finalizables: MutableList<() -> Unit>) { override fun addElementToInsert(insert: InsertStatement, entry: UUID, finalizables: MutableList<() -> Unit>) {
insert[table.uuid] = entry insert[table.uuid] = entry
} }
}) }
)
fun start(rpcOps: RPCOps, userService: RPCUserService) { fun start(rpcOps: RPCOps, userService: RPCUserService) {
state.locked { state.locked {
@ -257,7 +262,7 @@ class NodeMessagingClient(override val config: NodeConfiguration,
// Use the magic deduplication property built into Artemis as our message identity too // Use the magic deduplication property built into Artemis as our message identity too
val uuid = UUID.fromString(message.getStringProperty(HDR_DUPLICATE_DETECTION_ID)) val uuid = UUID.fromString(message.getStringProperty(HDR_DUPLICATE_DETECTION_ID))
val user = requireNotNull(message.getStringProperty(HDR_VALIDATED_USER)) { "Message is not authenticated" } val user = requireNotNull(message.getStringProperty(HDR_VALIDATED_USER)) { "Message is not authenticated" }
log.info("Received message from: ${message.address} user: $user topic: $topic sessionID: $sessionID uuid: $uuid") log.trace { "Received message from: ${message.address} user: $user topic: $topic sessionID: $sessionID uuid: $uuid" }
val body = ByteArray(message.bodySize).apply { message.bodyBuffer.readBytes(this) } val body = ByteArray(message.bodySize).apply { message.bodyBuffer.readBytes(this) }
@ -372,11 +377,11 @@ class NodeMessagingClient(override val config: NodeConfiguration,
// For demo purposes - if set then add a delay to messages in order to demonstrate that the flows are doing as intended // For demo purposes - if set then add a delay to messages in order to demonstrate that the flows are doing as intended
if (AMQ_DELAY > 0 && message.topicSession.topic == StateMachineManager.sessionTopic.topic) { if (AMQ_DELAY > 0 && message.topicSession.topic == StateMachineManager.sessionTopic.topic) {
putLongProperty(HDR_SCHEDULED_DELIVERY_TIME, System.currentTimeMillis() + AMQ_DELAY); putLongProperty(HDR_SCHEDULED_DELIVERY_TIME, System.currentTimeMillis() + AMQ_DELAY)
} }
} }
log.info("Send to: $mqAddress topic: ${message.topicSession.topic} sessionID: ${message.topicSession.sessionID} " + log.trace { "Send to: $mqAddress topic: ${message.topicSession.topic} " +
"uuid: ${message.uniqueMessageId}") "sessionID: ${message.topicSession.sessionID} uuid: ${message.uniqueMessageId}" }
producer!!.send(mqAddress, artemisMessage) producer!!.send(mqAddress, artemisMessage)
} }
} }

View File

@ -1,7 +1,6 @@
package net.corda.node.services.statemachine package net.corda.node.services.statemachine
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.node.services.statemachine.StateMachineManager.FlowSession
// TODO revisit when Kotlin 1.1 is released and data classes can extend other classes // TODO revisit when Kotlin 1.1 is released and data classes can extend other classes
interface FlowIORequest { interface FlowIORequest {
@ -22,17 +21,20 @@ interface SendRequest : SessionedFlowIORequest {
interface ReceiveRequest<T : SessionMessage> : SessionedFlowIORequest, WaitingRequest { interface ReceiveRequest<T : SessionMessage> : SessionedFlowIORequest, WaitingRequest {
val receiveType: Class<T> val receiveType: Class<T>
val userReceiveType: Class<*>?
} }
data class SendAndReceive<T : SessionMessage>(override val session: FlowSession, data class SendAndReceive<T : SessionMessage>(override val session: FlowSession,
override val message: SessionMessage, override val message: SessionMessage,
override val receiveType: Class<T>) : SendRequest, ReceiveRequest<T> { override val receiveType: Class<T>,
override val userReceiveType: Class<*>?) : SendRequest, ReceiveRequest<T> {
@Transient @Transient
override val stackTraceInCaseOfProblems: StackSnapshot = StackSnapshot() override val stackTraceInCaseOfProblems: StackSnapshot = StackSnapshot()
} }
data class ReceiveOnly<T : SessionMessage>(override val session: FlowSession, data class ReceiveOnly<T : SessionMessage>(override val session: FlowSession,
override val receiveType: Class<T>) : ReceiveRequest<T> { override val receiveType: Class<T>,
override val userReceiveType: Class<*>?) : ReceiveRequest<T> {
@Transient @Transient
override val stackTraceInCaseOfProblems: StackSnapshot = StackSnapshot() override val stackTraceInCaseOfProblems: StackSnapshot = StackSnapshot()
} }

View File

@ -0,0 +1,44 @@
package net.corda.node.services.statemachine
import net.corda.core.crypto.Party
import net.corda.core.flows.FlowLogic
import net.corda.node.services.statemachine.FlowSessionState.Initiated
import net.corda.node.services.statemachine.FlowSessionState.Initiating
import java.util.concurrent.ConcurrentLinkedQueue
class FlowSession(
val flow: FlowLogic<*>,
val ourSessionId: Long,
val initiatingParty: Party?,
var state: FlowSessionState)
{
val receivedMessages = ConcurrentLinkedQueue<ReceivedSessionMessage<*>>()
val fiber: FlowStateMachineImpl<*> get() = flow.stateMachine as FlowStateMachineImpl<*>
override fun toString(): String {
return "${javaClass.simpleName}(flow=$flow, ourSessionId=$ourSessionId, initiatingParty=$initiatingParty, state=$state)"
}
}
/**
* [FlowSessionState] describes the session's state.
*
* [Initiating] is pre-handshake. [Initiating.otherParty] at this point holds a [Party] corresponding to either a
* specific peer or a service.
* [Initiated] is post-handshake. At this point [Initiating.otherParty] will have been resolved to a specific peer
* [Initiated.peerParty], and the peer's sessionId has been initialised.
*/
sealed class FlowSessionState {
abstract val sendToParty: Party
/** [otherParty] may be a specific peer or a service party */
class Initiating(val otherParty: Party) : FlowSessionState() {
override val sendToParty: Party get() = otherParty
override fun toString(): String = "${javaClass.simpleName}($otherParty)"
}
class Initiated(val peerParty: Party, val peerSessionId: Long) : FlowSessionState() {
override val sendToParty: Party get() = peerParty
override fun toString(): String = "${javaClass.simpleName}($peerParty, $peerSessionId)"
}
}

View File

@ -6,6 +6,7 @@ import co.paralleluniverse.fibers.Suspendable
import co.paralleluniverse.strands.Strand import co.paralleluniverse.strands.Strand
import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.ListenableFuture
import com.google.common.util.concurrent.SettableFuture import com.google.common.util.concurrent.SettableFuture
import net.corda.core.abbreviate
import net.corda.core.crypto.Party import net.corda.core.crypto.Party
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowException import net.corda.core.flows.FlowException
@ -15,10 +16,9 @@ import net.corda.core.flows.StateMachineRunId
import net.corda.core.random63BitValue import net.corda.core.random63BitValue
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.UntrustworthyData import net.corda.core.utilities.UntrustworthyData
import net.corda.core.utilities.debug
import net.corda.core.utilities.trace import net.corda.core.utilities.trace
import net.corda.node.services.api.ServiceHubInternal import net.corda.node.services.api.ServiceHubInternal
import net.corda.node.services.statemachine.StateMachineManager.FlowSession
import net.corda.node.services.statemachine.StateMachineManager.FlowSessionState
import net.corda.node.utilities.StrandLocalTransactionManager import net.corda.node.utilities.StrandLocalTransactionManager
import net.corda.node.utilities.createDatabaseTransaction import net.corda.node.utilities.createDatabaseTransaction
import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.Database
@ -28,11 +28,10 @@ import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.sql.SQLException import java.sql.SQLException
import java.util.* import java.util.*
import java.util.concurrent.ExecutionException
class FlowStateMachineImpl<R>(override val id: StateMachineRunId, class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
val logic: FlowLogic<R>, val logic: FlowLogic<R>,
scheduler: FiberScheduler) : Fiber<Unit>("flow", scheduler), FlowStateMachine<R> { scheduler: FiberScheduler) : Fiber<Unit>(id.toString(), scheduler), FlowStateMachine<R> {
companion object { companion object {
// Used to work around a small limitation in Quasar. // Used to work around a small limitation in Quasar.
private val QUASAR_UNBLOCKER = run { private val QUASAR_UNBLOCKER = run {
@ -56,9 +55,13 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
@Transient private var txTrampoline: Transaction? = null @Transient private var txTrampoline: Transaction? = null
@Transient private var _logger: Logger? = null @Transient private var _logger: Logger? = null
/**
* Return the logger for this state machine. The logger name incorporates [id] and so including this in the log
* message is not necessary.
*/
override val logger: Logger get() { override val logger: Logger get() {
return _logger ?: run { return _logger ?: run {
val l = LoggerFactory.getLogger(id.toString()) val l = LoggerFactory.getLogger("net.corda.flow.$id")
_logger = l _logger = l
return l return l
} }
@ -80,12 +83,12 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
init { init {
logic.stateMachine = this logic.stateMachine = this
name = id.toString()
} }
@Suspendable @Suspendable
override fun run() { override fun run() {
createTransaction() createTransaction()
logger.debug { "Calling flow: $logic" }
val result = try { val result = try {
logic.call() logic.call()
} catch (e: FlowException) { } catch (e: FlowException) {
@ -93,11 +96,13 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
val propagated = e.stackTrace[0].className == javaClass.name val propagated = e.stackTrace[0].className == javaClass.name
actionOnEnd(e, propagated) actionOnEnd(e, propagated)
_resultFuture?.setException(e) _resultFuture?.setException(e)
logger.debug(if (propagated) "Flow ended due to receiving exception" else "Flow finished with exception", e)
return return
} catch (t: Throwable) { } catch (t: Throwable) {
logger.warn("Terminated by unexpected exception", t)
actionOnEnd(t, false) actionOnEnd(t, false)
_resultFuture?.setException(t) _resultFuture?.setException(t)
throw ExecutionException(t) return
} }
// Only sessions which have a single send and nothing else will block here // Only sessions which have a single send and nothing else will block here
@ -107,6 +112,7 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
// This is to prevent actionOnEnd being called twice if it throws an exception // This is to prevent actionOnEnd being called twice if it throws an exception
actionOnEnd(null, false) actionOnEnd(null, false)
_resultFuture?.set(result) _resultFuture?.set(result)
logger.debug { "Flow finished with result $result" }
} }
private fun createTransaction() { private fun createTransaction() {
@ -134,27 +140,35 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
otherParty: Party, otherParty: Party,
payload: Any, payload: Any,
sessionFlow: FlowLogic<*>): UntrustworthyData<T> { sessionFlow: FlowLogic<*>): UntrustworthyData<T> {
logger.debug { "sendAndReceive(${receiveType.name}, $otherParty, ${payload.toString().abbreviate(300)}) ..." }
val session = getConfirmedSession(otherParty, sessionFlow) val session = getConfirmedSession(otherParty, sessionFlow)
return if (session == null) { val sessionData = if (session == null) {
val newSession = startNewSession(otherParty, sessionFlow, payload, waitForConfirmation = true) val newSession = startNewSession(otherParty, sessionFlow, payload, waitForConfirmation = true)
// Only do a receive here as the session init has carried the payload // Only do a receive here as the session init has carried the payload
receiveInternal<SessionData>(newSession, receiveType) receiveInternal<SessionData>(newSession, receiveType)
} else { } else {
sendAndReceiveInternal<SessionData>(session, createSessionData(session, payload), receiveType) val sendData = createSessionData(session, payload)
}.checkPayloadIs(receiveType) sendAndReceiveInternal<SessionData>(session, sendData, receiveType)
}
logger.debug { "Received ${sessionData.message.payload.toString().abbreviate(300)}" }
return sessionData.checkPayloadIs(receiveType)
} }
@Suspendable @Suspendable
override fun <T : Any> receive(receiveType: Class<T>, override fun <T : Any> receive(receiveType: Class<T>,
otherParty: Party, otherParty: Party,
sessionFlow: FlowLogic<*>): UntrustworthyData<T> { sessionFlow: FlowLogic<*>): UntrustworthyData<T> {
logger.debug { "receive(${receiveType.name}, $otherParty) ..." }
val session = getConfirmedSession(otherParty, sessionFlow) ?: val session = getConfirmedSession(otherParty, sessionFlow) ?:
startNewSession(otherParty, sessionFlow, null, waitForConfirmation = true) startNewSession(otherParty, sessionFlow, null, waitForConfirmation = true)
return receiveInternal<SessionData>(session, receiveType).checkPayloadIs(receiveType) val sessionData = receiveInternal<SessionData>(session, receiveType)
logger.debug { "Received ${sessionData.message.payload.toString().abbreviate(300)}" }
return sessionData.checkPayloadIs(receiveType)
} }
@Suspendable @Suspendable
override fun send(otherParty: Party, payload: Any, sessionFlow: FlowLogic<*>) { override fun send(otherParty: Party, payload: Any, sessionFlow: FlowLogic<*>) {
logger.debug { "send($otherParty, ${payload.toString().abbreviate(300)})" }
val session = getConfirmedSession(otherParty, sessionFlow) val session = getConfirmedSession(otherParty, sessionFlow)
if (session == null) { if (session == null) {
// Don't send the payload again if it was already piggy-backed on a session init // Don't send the payload again if it was already piggy-backed on a session init
@ -164,6 +178,27 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
} }
} }
@Suspendable
override fun waitForLedgerCommit(hash: SecureHash, sessionFlow: FlowLogic<*>): SignedTransaction {
logger.debug { "waitForLedgerCommit($hash) ..." }
suspend(WaitForLedgerCommit(hash, sessionFlow.stateMachine as FlowStateMachineImpl<*>))
val stx = serviceHub.storageService.validatedTransactions.getTransaction(hash)
if (stx != null) {
logger.debug { "Transaction $hash committed to ledger" }
return stx
}
// If the tx isn't committed then we may have been resumed due to an session ending in an error
for (session in openSessions.values) {
for (receivedMessage in session.receivedMessages) {
if (receivedMessage.message is ErrorSessionEnd) {
session.erroredEnd(receivedMessage.message)
}
}
}
throw IllegalStateException("We were resumed after waiting for $hash but it wasn't found in our local storage")
}
/** /**
* This method will suspend the state machine and wait for incoming session init response from other party. * This method will suspend the state machine and wait for incoming session init response from other party.
*/ */
@ -178,23 +213,6 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
} }
} }
@Suspendable
override fun waitForLedgerCommit(hash: SecureHash, sessionFlow: FlowLogic<*>): SignedTransaction {
logger.info("Waiting for transaction $hash to commit")
suspend(WaitForLedgerCommit(hash, sessionFlow.stateMachine as FlowStateMachineImpl<*>))
val stx = serviceHub.storageService.validatedTransactions.getTransaction(hash)
if (stx != null) return stx
// If the tx isn't committed then we may have been resumed due to an session ending in an error
for (session in openSessions.values) {
for (receivedMessage in session.receivedMessages) {
if (receivedMessage.message is ErrorSessionEnd) {
session.erroredEnd(receivedMessage.message)
}
}
}
throw IllegalStateException("We were resumed after waiting for $hash but it wasn't found in our local storage")
}
private fun createSessionData(session: FlowSession, payload: Any): SessionData { private fun createSessionData(session: FlowSession, payload: Any): SessionData {
val sessionState = session.state val sessionState = session.state
val peerSessionId = when (sessionState) { val peerSessionId = when (sessionState) {
@ -212,14 +230,14 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
private inline fun <reified M : ExistingSessionMessage> receiveInternal( private inline fun <reified M : ExistingSessionMessage> receiveInternal(
session: FlowSession, session: FlowSession,
userReceiveType: Class<*>?): ReceivedSessionMessage<M> { userReceiveType: Class<*>?): ReceivedSessionMessage<M> {
return waitForMessage(ReceiveOnly(session, M::class.java), userReceiveType) return waitForMessage(ReceiveOnly(session, M::class.java, userReceiveType))
} }
private inline fun <reified M : ExistingSessionMessage> sendAndReceiveInternal( private inline fun <reified M : ExistingSessionMessage> sendAndReceiveInternal(
session: FlowSession, session: FlowSession,
message: SessionMessage, message: SessionMessage,
userReceiveType: Class<*>?): ReceivedSessionMessage<M> { userReceiveType: Class<*>?): ReceivedSessionMessage<M> {
return waitForMessage(SendAndReceive(session, message, M::class.java), userReceiveType) return waitForMessage(SendAndReceive(session, message, M::class.java, userReceiveType))
} }
@Suspendable @Suspendable
@ -253,11 +271,9 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
} }
@Suspendable @Suspendable
private fun <M : ExistingSessionMessage> waitForMessage(
receiveRequest: ReceiveRequest<M>, private fun <M : ExistingSessionMessage> waitForMessage(receiveRequest: ReceiveRequest<M>): ReceivedSessionMessage<M> {
userReceiveType: Class<*>?): ReceivedSessionMessage<M> { return receiveRequest.suspendAndExpectReceive().confirmReceiveType(receiveRequest)
val receivedMessage = receiveRequest.suspendAndExpectReceive()
return receivedMessage.confirmReceiveType(receiveRequest, userReceiveType)
} }
@Suspendable @Suspendable
@ -280,8 +296,7 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
} }
private fun <M : ExistingSessionMessage> ReceivedSessionMessage<*>.confirmReceiveType( private fun <M : ExistingSessionMessage> ReceivedSessionMessage<*>.confirmReceiveType(
receiveRequest: ReceiveRequest<M>, receiveRequest: ReceiveRequest<M>): ReceivedSessionMessage<M> {
userReceiveType: Class<*>?): ReceivedSessionMessage<M> {
val session = receiveRequest.session val session = receiveRequest.session
val receiveType = receiveRequest.receiveType val receiveType = receiveRequest.receiveType
if (receiveType.isInstance(message)) { if (receiveType.isInstance(message)) {
@ -292,7 +307,7 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
if (message is ErrorSessionEnd) { if (message is ErrorSessionEnd) {
session.erroredEnd(message) session.erroredEnd(message)
} else { } else {
val expectedType = userReceiveType?.name ?: receiveType.simpleName val expectedType = receiveRequest.userReceiveType?.name ?: receiveType.simpleName
throw FlowSessionException("Counterparty flow on ${session.state.sendToParty} has completed without " + throw FlowSessionException("Counterparty flow on ${session.state.sendToParty} has completed without " +
"sending a $expectedType") "sending a $expectedType")
} }
@ -310,7 +325,7 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
throw FlowSessionException("Counterparty flow on ${state.sendToParty} had an internal error and has terminated") throw FlowSessionException("Counterparty flow on ${state.sendToParty} had an internal error and has terminated")
} }
} }
@Suspendable @Suspendable
private fun suspend(ioRequest: FlowIORequest) { private fun suspend(ioRequest: FlowIORequest) {
// We have to pass the thread local database transaction across via a transient field as the fiber park // We have to pass the thread local database transaction across via a transient field as the fiber park
@ -321,7 +336,7 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
waitingForResponse = ioRequest waitingForResponse = ioRequest
var exceptionDuringSuspend: Throwable? = null var exceptionDuringSuspend: Throwable? = null
parkAndSerialize { fiber, serializer -> parkAndSerialize { f, s ->
logger.trace { "Suspended on $ioRequest" } logger.trace { "Suspended on $ioRequest" }
// restore the Tx onto the ThreadLocal so that we can commit the ensuing checkpoint to the DB // restore the Tx onto the ThreadLocal so that we can commit the ensuing checkpoint to the DB
try { try {
@ -332,6 +347,7 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
// Quasar does not terminate the fiber properly if an exception occurs during a suspend. We have to // Quasar does not terminate the fiber properly if an exception occurs during a suspend. We have to
// resume the fiber just so that we can throw it when it's running. // resume the fiber just so that we can throw it when it's running.
exceptionDuringSuspend = t exceptionDuringSuspend = t
logger.trace("Resuming so fiber can it terminate with the exception thrown during suspend process", t)
resume(scheduler) resume(scheduler)
} }
} }
@ -353,7 +369,6 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
logger.trace("Started") logger.trace("Started")
start() start()
} else { } else {
logger.trace("Resumed")
Fiber.unpark(this, QUASAR_UNBLOCKER) Fiber.unpark(this, QUASAR_UNBLOCKER)
} }
} catch (t: Throwable) { } catch (t: Throwable) {

View File

@ -1,6 +1,5 @@
package net.corda.node.services.statemachine package net.corda.node.services.statemachine
import net.corda.core.abbreviate
import net.corda.core.crypto.Party import net.corda.core.crypto.Party
import net.corda.core.flows.FlowException import net.corda.core.flows.FlowException
import net.corda.core.utilities.UntrustworthyData import net.corda.core.utilities.UntrustworthyData
@ -14,9 +13,7 @@ interface ExistingSessionMessage : SessionMessage {
} }
data class SessionData(override val recipientSessionId: Long, val payload: Any) : ExistingSessionMessage { data class SessionData(override val recipientSessionId: Long, val payload: Any) : ExistingSessionMessage {
override fun toString(): String { override fun toString(): String = "${javaClass.simpleName}(recipientSessionId=$recipientSessionId, payload=$payload)"
return "${javaClass.simpleName}(recipientSessionId=$recipientSessionId, payload=${payload.toString().abbreviate(100)})"
}
} }
interface SessionInitResponse : ExistingSessionMessage { interface SessionInitResponse : ExistingSessionMessage {

View File

@ -31,8 +31,6 @@ import net.corda.core.utilities.trace
import net.corda.node.services.api.Checkpoint import net.corda.node.services.api.Checkpoint
import net.corda.node.services.api.CheckpointStorage import net.corda.node.services.api.CheckpointStorage
import net.corda.node.services.api.ServiceHubInternal import net.corda.node.services.api.ServiceHubInternal
import net.corda.node.services.statemachine.StateMachineManager.FlowSessionState.Initiated
import net.corda.node.services.statemachine.StateMachineManager.FlowSessionState.Initiating
import net.corda.node.utilities.* import net.corda.node.utilities.*
import org.apache.activemq.artemis.utils.ReusableLatch import org.apache.activemq.artemis.utils.ReusableLatch
import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.Database
@ -40,8 +38,6 @@ import rx.Observable
import rx.subjects.PublishSubject import rx.subjects.PublishSubject
import java.util.* import java.util.*
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.ExecutionException
import javax.annotation.concurrent.ThreadSafe import javax.annotation.concurrent.ThreadSafe
/** /**
@ -79,6 +75,12 @@ class StateMachineManager(val serviceHub: ServiceHubInternal,
companion object { companion object {
private val logger = loggerFor<StateMachineManager>() private val logger = loggerFor<StateMachineManager>()
internal val sessionTopic = TopicSession("platform.session") internal val sessionTopic = TopicSession("platform.session")
init {
Fiber.setDefaultUncaughtExceptionHandler { fiber, throwable ->
(fiber as FlowStateMachineImpl<*>).logger.error("Caught exception from flow", throwable)
}
}
} }
val scheduler = FiberScheduler() val scheduler = FiberScheduler()
@ -148,12 +150,6 @@ class StateMachineManager(val serviceHub: ServiceHubInternal,
*/ */
val changes: Observable<Change> = mutex.content.changesPublisher.wrapWithDatabaseTransaction() val changes: Observable<Change> = mutex.content.changesPublisher.wrapWithDatabaseTransaction()
init {
Fiber.setDefaultUncaughtExceptionHandler { fiber, throwable ->
(fiber as FlowStateMachineImpl<*>).logger.error("Caught exception from flow", throwable)
}
}
fun start() { fun start() {
restoreFibersFromCheckpoints() restoreFibersFromCheckpoints()
listenToLedgerTransactions() listenToLedgerTransactions()
@ -168,7 +164,7 @@ class StateMachineManager(val serviceHub: ServiceHubInternal,
if (fibers.isNotEmpty()) { if (fibers.isNotEmpty()) {
executor.executeASAP { executor.executeASAP {
for (fiber in fibers) { for (fiber in fibers) {
fiber.logger.info("Transaction $hash has committed to the ledger, resuming") fiber.logger.trace { "Transaction $hash has committed to the ledger, resuming" }
fiber.waitingForResponse = null fiber.waitingForResponse = null
resumeFiber(fiber) resumeFiber(fiber)
} }
@ -246,7 +242,7 @@ class StateMachineManager(val serviceHub: ServiceHubInternal,
serviceHub.storageService.validatedTransactions.getTransaction(waitingForResponse.hash) serviceHub.storageService.validatedTransactions.getTransaction(waitingForResponse.hash)
} }
if (stx != null) { if (stx != null) {
fiber.logger.info("Resuming fiber as tx ${waitingForResponse.hash} has committed.") fiber.logger.info("Resuming fiber as tx ${waitingForResponse.hash} has committed")
fiber.waitingForResponse = null fiber.waitingForResponse = null
resumeFiber(fiber) resumeFiber(fiber)
} else { } else {
@ -288,14 +284,14 @@ class StateMachineManager(val serviceHub: ServiceHubInternal,
// before then. // before then.
session.fiber.waitingForResponse = null session.fiber.waitingForResponse = null
updateCheckpoint(session.fiber) updateCheckpoint(session.fiber)
session.fiber.logger.debug { "About to resume due to $message" } session.fiber.logger.trace { "Resuming due to $message" }
resumeFiber(session.fiber) resumeFiber(session.fiber)
} }
} else { } else {
val peerParty = recentlyClosedSessions.remove(message.recipientSessionId) val peerParty = recentlyClosedSessions.remove(message.recipientSessionId)
if (peerParty != null) { if (peerParty != null) {
if (message is SessionConfirm) { if (message is SessionConfirm) {
logger.debug { "Received session confirmation but associated fiber has already terminated, so sending session end" } logger.trace { "Received session confirmation but associated fiber has already terminated, so sending session end" }
sendSessionMessage(peerParty, NormalSessionEnd(message.initiatedSessionId)) sendSessionMessage(peerParty, NormalSessionEnd(message.initiatedSessionId))
} else { } else {
logger.trace { "Ignoring session end message for already closed session: $message" } logger.trace { "Ignoring session end message for already closed session: $message" }
@ -315,7 +311,7 @@ class StateMachineManager(val serviceHub: ServiceHubInternal,
} }
private fun onSessionInit(sessionInit: SessionInit, sender: Party) { private fun onSessionInit(sessionInit: SessionInit, sender: Party) {
logger.trace { "Received $sessionInit $sender" } logger.trace { "Received $sessionInit from $sender" }
val otherPartySessionId = sessionInit.initiatorSessionId val otherPartySessionId = sessionInit.initiatorSessionId
fun sendSessionReject(message: String) = sendSessionMessage(sender, SessionReject(otherPartySessionId, message)) fun sendSessionReject(message: String) = sendSessionMessage(sender, SessionReject(otherPartySessionId, message))
@ -347,14 +343,15 @@ class StateMachineManager(val serviceHub: ServiceHubInternal,
updateCheckpoint(fiber) updateCheckpoint(fiber)
session session
} catch (e: Exception) { } catch (e: Exception) {
logger.warn("Couldn't start session for $sessionInit", e) logger.warn("Couldn't start flow session from $sessionInit", e)
sendSessionReject("Unable to establish session") sendSessionReject("Unable to establish session")
return return
} }
sendSessionMessage(sender, SessionConfirm(otherPartySessionId, session.ourSessionId), session.fiber) sendSessionMessage(sender, SessionConfirm(otherPartySessionId, session.ourSessionId), session.fiber)
session.fiber.logger.debug { "Initiated from $sessionInit on $session" } session.fiber.logger.debug { "Initiated by $sender using marker ${markerClass.name}" }
startFiber(session.fiber) session.fiber.logger.trace { "Initiated from $sessionInit on $session" }
resumeFiber(session.fiber)
} }
private fun serializeFiber(fiber: FlowStateMachineImpl<*>): SerializedBytes<FlowStateMachineImpl<*>> { private fun serializeFiber(fiber: FlowStateMachineImpl<*>): SerializedBytes<FlowStateMachineImpl<*>> {
@ -426,7 +423,7 @@ class StateMachineManager(val serviceHub: ServiceHubInternal,
} }
private fun FlowSession.endSession(exception: Throwable?, propagated: Boolean) { private fun FlowSession.endSession(exception: Throwable?, propagated: Boolean) {
val initiatedState = state as? Initiated ?: return val initiatedState = state as? FlowSessionState.Initiated ?: return
val sessionEnd = if (exception == null) { val sessionEnd = if (exception == null) {
NormalSessionEnd(initiatedState.peerSessionId) NormalSessionEnd(initiatedState.peerSessionId)
} else { } else {
@ -443,25 +440,6 @@ class StateMachineManager(val serviceHub: ServiceHubInternal,
recentlyClosedSessions[ourSessionId] = initiatedState.peerParty recentlyClosedSessions[ourSessionId] = initiatedState.peerParty
} }
private fun startFiber(fiber: FlowStateMachineImpl<*>) {
try {
resumeFiber(fiber)
} catch (e: ExecutionException) {
// There are two ways we can take exceptions in this method:
//
// 1) A bug in the SMM code itself whilst setting up the new flow. In that case the exception will
// propagate out of this method as it would for any method.
//
// 2) An exception in the first part of the fiber after it's been invoked for the first time via
// fiber.start(). In this case the exception will be caught and stashed in the flow logic future,
// then sent to the unhandled exception handler above which logs it, and is then rethrown wrapped
// in an ExecutionException or RuntimeException+EE so we can just catch it here and ignore it.
} catch (e: RuntimeException) {
if (e.cause !is ExecutionException)
throw e
}
}
/** /**
* Kicks off a brand new state machine of the given class. * Kicks off a brand new state machine of the given class.
* The state machine will be persisted when it suspends, with automated restart if the StateMachineManager is * The state machine will be persisted when it suspends, with automated restart if the StateMachineManager is
@ -484,7 +462,7 @@ class StateMachineManager(val serviceHub: ServiceHubInternal,
// If we are not started then our checkpoint will be picked up during start // If we are not started then our checkpoint will be picked up during start
mutex.locked { mutex.locked {
if (started) { if (started) {
startFiber(fiber) resumeFiber(fiber)
} }
} }
return fiber return fiber
@ -509,7 +487,7 @@ class StateMachineManager(val serviceHub: ServiceHubInternal,
fiber.resume(scheduler) fiber.resume(scheduler)
} }
} else { } else {
fiber.logger.debug("Not resuming as SMM is stopping.") fiber.logger.trace("Not resuming as SMM is stopping.")
decrementLiveFibers() decrementLiveFibers()
} }
} }
@ -551,40 +529,7 @@ class StateMachineManager(val serviceHub: ServiceHubInternal,
?: throw IllegalArgumentException("Don't know about party $party") ?: throw IllegalArgumentException("Don't know about party $party")
val address = serviceHub.networkService.getAddressOfParty(partyInfo) val address = serviceHub.networkService.getAddressOfParty(partyInfo)
val logger = fiber?.logger ?: logger val logger = fiber?.logger ?: logger
logger.debug { "Sending $message to party $party, address: $address" } logger.trace { "Sending $message to party $party @ $address" }
serviceHub.networkService.send(sessionTopic, message, address) serviceHub.networkService.send(sessionTopic, message, address)
} }
/**
* [FlowSessionState] describes the session's state.
*
* [Initiating] is pre-handshake. [Initiating.otherParty] at this point holds a [Party] corresponding to either a
* specific peer or a service.
* [Initiated] is post-handshake. At this point [Initiating.otherParty] will have been resolved to a specific peer
* [Initiated.peerParty], and the peer's sessionId has been initialised.
*/
sealed class FlowSessionState {
abstract val sendToParty: Party
class Initiating(
val otherParty: Party /** This may be a specific peer or a service party */
) : FlowSessionState() {
override val sendToParty: Party get() = otherParty
}
class Initiated(
val peerParty: Party, /** This must be a peer party */
val peerSessionId: Long
) : FlowSessionState() {
override val sendToParty: Party get() = peerParty
}
}
data class FlowSession(
val flow: FlowLogic<*>,
val ourSessionId: Long,
val initiatingParty: Party?,
var state: FlowSessionState)
{
val receivedMessages = ConcurrentLinkedQueue<ReceivedSessionMessage<*>>()
val fiber: FlowStateMachineImpl<*> get() = flow.stateMachine as FlowStateMachineImpl<*>
}
} }

View File

@ -5,6 +5,7 @@ import net.corda.core.div
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatExceptionOfType import org.assertj.core.api.Assertions.assertThatExceptionOfType
import org.junit.Test import org.junit.Test
import org.slf4j.event.Level
import java.nio.file.Paths import java.nio.file.Paths
class ArgsParserTest { class ArgsParserTest {
@ -18,65 +19,59 @@ class ArgsParserTest {
configFile = workingDirectory / "node.conf", configFile = workingDirectory / "node.conf",
help = false, help = false,
logToConsole = false, logToConsole = false,
loggingLevel = Level.INFO,
isWebserver = false)) isWebserver = false))
} }
@Test @Test
fun `just base-directory with relative path`() { fun `base-directory with relative path`() {
val expectedBaseDir = Paths.get("tmp").normalize().toAbsolutePath() val expectedBaseDir = Paths.get("tmp").normalize().toAbsolutePath()
val cmdLineOptions = parser.parse("--base-directory", "tmp") val cmdLineOptions = parser.parse("--base-directory", "tmp")
assertThat(cmdLineOptions).isEqualTo(CmdLineOptions( assertThat(cmdLineOptions.baseDirectory).isEqualTo(expectedBaseDir)
baseDirectory = expectedBaseDir, assertThat(cmdLineOptions.configFile).isEqualTo(expectedBaseDir / "node.conf")
configFile = expectedBaseDir / "node.conf",
help = false,
logToConsole = false,
isWebserver = false))
} }
@Test @Test
fun `just base-directory with absolute path`() { fun `base-directory with absolute path`() {
val baseDirectory = Paths.get("tmp").normalize().toAbsolutePath() val baseDirectory = Paths.get("tmp").normalize().toAbsolutePath()
val cmdLineOptions = parser.parse("--base-directory", baseDirectory.toString()) val cmdLineOptions = parser.parse("--base-directory", baseDirectory.toString())
assertThat(cmdLineOptions).isEqualTo(CmdLineOptions( assertThat(cmdLineOptions.baseDirectory).isEqualTo(baseDirectory)
baseDirectory = baseDirectory, assertThat(cmdLineOptions.configFile).isEqualTo(baseDirectory / "node.conf")
configFile = baseDirectory / "node.conf",
help = false,
logToConsole = false,
isWebserver = false))
} }
@Test @Test
fun `just config-file with relative path`() { fun `config-file with relative path`() {
val cmdLineOptions = parser.parse("--config-file", "different.conf") val cmdLineOptions = parser.parse("--config-file", "different.conf")
assertThat(cmdLineOptions).isEqualTo(CmdLineOptions( assertThat(cmdLineOptions.baseDirectory).isEqualTo(workingDirectory)
baseDirectory = workingDirectory, assertThat(cmdLineOptions.configFile).isEqualTo(workingDirectory / "different.conf")
configFile = workingDirectory / "different.conf",
help = false,
logToConsole = false,
isWebserver = false))
} }
@Test @Test
fun `just config-file with absolute path`() { fun `config-file with absolute path`() {
val configFile = Paths.get("tmp", "a.conf").normalize().toAbsolutePath() val configFile = Paths.get("tmp", "a.conf").normalize().toAbsolutePath()
val cmdLineOptions = parser.parse("--config-file", configFile.toString()) val cmdLineOptions = parser.parse("--config-file", configFile.toString())
assertThat(cmdLineOptions).isEqualTo(CmdLineOptions( assertThat(cmdLineOptions.baseDirectory).isEqualTo(workingDirectory)
baseDirectory = workingDirectory, assertThat(cmdLineOptions.configFile).isEqualTo(configFile)
configFile = configFile,
help = false,
logToConsole = false,
isWebserver = false))
} }
@Test @Test
fun `just webserver `() { fun `log-to-console`() {
val cmdLineOptions = parser.parse("--log-to-console")
assertThat(cmdLineOptions.logToConsole).isTrue()
}
@Test
fun `logging-level`() {
for (level in Level.values()) {
val cmdLineOptions = parser.parse("--logging-level", level.name)
assertThat(cmdLineOptions.loggingLevel).isEqualTo(level)
}
}
@Test
fun `webserver`() {
val cmdLineOptions = parser.parse("--webserver") val cmdLineOptions = parser.parse("--webserver")
assertThat(cmdLineOptions).isEqualTo(CmdLineOptions( assertThat(cmdLineOptions.isWebserver).isTrue()
baseDirectory = workingDirectory,
configFile = workingDirectory / "node.conf",
help = false,
logToConsole = false,
isWebserver = true))
} }
@Test @Test
@ -99,4 +94,18 @@ class ArgsParserTest {
parser.parse("--config-file") parser.parse("--config-file")
}.withMessageContaining("config-file") }.withMessageContaining("config-file")
} }
@Test
fun `logging-level without argument`() {
assertThatExceptionOfType(OptionException::class.java).isThrownBy {
parser.parse("--logging-level")
}.withMessageContaining("logging-level")
}
@Test
fun `logging-level with invalid argument`() {
assertThatExceptionOfType(OptionException::class.java).isThrownBy {
parser.parse("--logging-level", "not-a-level")
}.withMessageContaining("logging-level")
}
} }

View File

@ -2,7 +2,6 @@ package net.corda.node.services.statemachine
import co.paralleluniverse.fibers.Fiber import co.paralleluniverse.fibers.Fiber
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import co.paralleluniverse.strands.Strand.UncaughtExceptionHandler
import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.ListenableFuture
import net.corda.core.contracts.DOLLARS import net.corda.core.contracts.DOLLARS
import net.corda.core.contracts.DummyState import net.corda.core.contracts.DummyState
@ -17,11 +16,11 @@ import net.corda.core.messaging.MessageRecipients
import net.corda.core.node.services.PartyInfo import net.corda.core.node.services.PartyInfo
import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceInfo
import net.corda.core.random63BitValue import net.corda.core.random63BitValue
import net.corda.core.rootCause
import net.corda.core.serialization.OpaqueBytes import net.corda.core.serialization.OpaqueBytes
import net.corda.core.serialization.deserialize import net.corda.core.serialization.deserialize
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.LogHelper
import net.corda.core.utilities.unwrap import net.corda.core.utilities.unwrap
import net.corda.flows.CashIssueFlow import net.corda.flows.CashIssueFlow
import net.corda.flows.CashPaymentFlow import net.corda.flows.CashPaymentFlow
@ -53,6 +52,12 @@ import kotlin.test.assertFailsWith
import kotlin.test.assertTrue import kotlin.test.assertTrue
class StateMachineManagerTests { class StateMachineManagerTests {
companion object {
init {
LogHelper.setLevel("+net.corda.flow")
}
}
private val net = MockNetwork(servicePeerAllocationStrategy = RoundRobin()) private val net = MockNetwork(servicePeerAllocationStrategy = RoundRobin())
private val sessionTransfers = ArrayList<SessionTransfer>() private val sessionTransfers = ArrayList<SessionTransfer>()
private lateinit var node1: MockNode private lateinit var node1: MockNode
@ -111,17 +116,11 @@ class StateMachineManagerTests {
fiber.actionOnSuspend = { fiber.actionOnSuspend = {
throw exceptionDuringSuspend throw exceptionDuringSuspend
} }
var uncaughtException: Throwable? = null
fiber.uncaughtExceptionHandler = UncaughtExceptionHandler { f, e ->
uncaughtException = e
}
net.runNetwork() net.runNetwork()
assertThatThrownBy { assertThatThrownBy {
fiber.resultFuture.getOrThrow() fiber.resultFuture.getOrThrow()
}.isSameAs(exceptionDuringSuspend) }.isSameAs(exceptionDuringSuspend)
assertThat(node1.smm.allStateMachines).isEmpty() assertThat(node1.smm.allStateMachines).isEmpty()
// Make sure it doesn't get swallowed up
assertThat(uncaughtException?.rootCause).isSameAs(exceptionDuringSuspend)
// Make sure the fiber does actually terminate // Make sure the fiber does actually terminate
assertThat(fiber.isTerminated).isTrue() assertThat(fiber.isTerminated).isTrue()
} }

View File

@ -13,7 +13,7 @@
<Console name="Console-Appender" target="SYSTEM_OUT"> <Console name="Console-Appender" target="SYSTEM_OUT">
<PatternLayout> <PatternLayout>
<pattern> <pattern>
[%-5level] %d{HH:mm:ss.SSS} [%t] %c{1}.%M - %msg%n [%-5level] %d{HH:mm:ss.SSS} [%t] %c{2}.%M - %msg%n
</pattern>> </pattern>>
</PatternLayout> </PatternLayout>
</Console> </Console>
@ -24,7 +24,7 @@
fileName="${log-path}/${log-name}.log" fileName="${log-path}/${log-name}.log"
filePattern="${archive}/${log-name}.%d{yyyy-MM-dd}-%i.log.gz"> filePattern="${archive}/${log-name}.%d{yyyy-MM-dd}-%i.log.gz">
<PatternLayout pattern="[%-5level] %d{ISO8601}{GMT+0} [%t] %c{1} - %msg%n"/> <PatternLayout pattern="[%-5level] %d{ISO8601}{GMT+0} [%t] %c{2} - %msg%n"/>
<Policies> <Policies>
<TimeBasedTriggeringPolicy/> <TimeBasedTriggeringPolicy/>

View File

@ -98,26 +98,24 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false,
* Because this executor is shared, we need to be careful about nodes shutting it down. * Because this executor is shared, we need to be careful about nodes shutting it down.
*/ */
private val sharedUserCount = AtomicInteger(0) private val sharedUserCount = AtomicInteger(0)
private val sharedServerThread = private val sharedServerThread = object : ServiceAffinityExecutor("Mock network", 1) {
object : ServiceAffinityExecutor("Mock network shared node thread", 1) { override fun shutdown() {
// We don't actually allow the shutdown of the network-wide shared thread pool until all references to
override fun shutdown() { // it have been shutdown.
// We don't actually allow the shutdown of the network-wide shared thread pool until all references to if (sharedUserCount.decrementAndGet() == 0) {
// it have been shutdown. super.shutdown()
if (sharedUserCount.decrementAndGet() == 0) {
super.shutdown()
}
}
override fun awaitTermination(timeout: Long, unit: TimeUnit): Boolean {
if (!isShutdown) {
flush()
return true
} else {
return super.awaitTermination(timeout, unit)
}
}
} }
}
override fun awaitTermination(timeout: Long, unit: TimeUnit): Boolean {
if (!isShutdown) {
flush()
return true
} else {
return super.awaitTermination(timeout, unit)
}
}
}
/** /**
* @param overrideServices a set of service entries to use in place of the node's default service entries, * @param overrideServices a set of service entries to use in place of the node's default service entries,