Added bits of versioning info to the node

This commit is contained in:
Shams Asari
2017-03-06 14:19:04 +00:00
parent 570b871524
commit e9d63b2662
23 changed files with 211 additions and 83 deletions

View File

@ -15,6 +15,7 @@ import net.corda.node.services.network.NetworkMapService
import net.corda.node.services.network.NetworkMapService.RegistrationRequest
import net.corda.node.services.network.NodeRegistration
import net.corda.node.utilities.AddOrRemove
import net.corda.testing.MOCK_VERSION
import net.corda.testing.TestNodeConfiguration
import net.corda.testing.node.NodeBasedTest
import net.corda.testing.node.SimpleNode
@ -62,7 +63,7 @@ class P2PSecurityTest : NodeBasedTest() {
}
private fun SimpleNode.registerWithNetworkMap(registrationName: String): ListenableFuture<NetworkMapService.RegistrationResponse> {
val nodeInfo = NodeInfo(net.myAddress, Party(registrationName, identity.public))
val nodeInfo = NodeInfo(net.myAddress, Party(registrationName, identity.public), MOCK_VERSION)
val registration = NodeRegistration(nodeInfo, System.currentTimeMillis(), AddOrRemove.ADD, Instant.MAX)
val request = RegistrationRequest(registration.toWire(identity.private), net.myAddress)
return net.sendRequest<NetworkMapService.RegistrationResponse>(NetworkMapService.REGISTER_TOPIC, request, networkMapNode.net.myAddress)

View File

@ -29,6 +29,7 @@ class ArgsParser {
.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 isRegistrationArg = optionParser.accepts("initial-registration", "Start initial node registration with Corda network to obtain certificate from the permissioning server.")
private val isVersionArg = optionParser.accepts("version", "Print the version and exit")
private val helpArg = optionParser.accepts("help").forHelp()
fun parse(vararg args: String): CmdLineOptions {
@ -42,7 +43,8 @@ class ArgsParser {
val loggingLevel = optionSet.valueOf(loggerLevel)
val logToConsole = optionSet.has(logToConsoleArg)
val isRegistration = optionSet.has(isRegistrationArg)
return CmdLineOptions(baseDirectory, configFile, help, loggingLevel, logToConsole, isRegistration)
val isVersion = optionSet.has(isVersionArg)
return CmdLineOptions(baseDirectory, configFile, help, loggingLevel, logToConsole, isRegistration, isVersion)
}
fun printHelp(sink: PrintStream) = optionParser.printHelpOn(sink)
@ -53,7 +55,8 @@ data class CmdLineOptions(val baseDirectory: Path,
val help: Boolean,
val loggingLevel: Level,
val logToConsole: Boolean,
val isRegistration: Boolean) {
val isRegistration: Boolean,
val isVersion: Boolean) {
fun loadConfig(allowMissingConfig: Boolean = false, configOverrides: Map<String, Any?> = emptyMap()): Config {
return ConfigHelper.loadConfig(baseDirectory, configFile, allowMissingConfig, configOverrides)
}

View File

@ -1,9 +1,12 @@
@file:JvmName("Corda")
package net.corda.node
import com.jcabi.manifests.Manifests
import com.typesafe.config.ConfigException
import joptsimple.OptionException
import net.corda.core.*
import net.corda.core.node.NodeVersionInfo
import net.corda.core.node.Version
import net.corda.core.utilities.Emoji
import net.corda.node.internal.Node
import net.corda.node.services.config.FullNodeConfiguration
@ -35,6 +38,16 @@ fun main(args: Array<String>) {
val startTime = System.currentTimeMillis()
checkJavaVersion()
val nodeVersionInfo = if (Manifests.exists("Corda-Version")) {
NodeVersionInfo(
Version.parse(Manifests.read("Corda-Version")),
Manifests.read("Corda-Revision"),
Manifests.read("Corda-Vendor"))
} else {
// If the manifest properties aren't available then we're running from within an IDE
NodeVersionInfo(Version(0, 0, false), "~Git revision unavailable~", "Unknown vendor")
}
val argsParser = ArgsParser()
val cmdlineOptions = try {
@ -45,6 +58,12 @@ fun main(args: Array<String>) {
exitProcess(1)
}
if (cmdlineOptions.isVersion) {
println("${nodeVersionInfo.vendor} ${nodeVersionInfo.version}")
println("Revision ${nodeVersionInfo.revision}")
exitProcess(0)
}
// Maybe render command line help.
if (cmdlineOptions.help) {
argsParser.printHelp(System.out)
@ -58,7 +77,7 @@ fun main(args: Array<String>) {
renderBasicInfoToConsole = false
}
drawBanner()
drawBanner(nodeVersionInfo)
System.setProperty("log-path", (cmdlineOptions.baseDirectory / "logs").toString())
@ -97,7 +116,7 @@ fun main(args: Array<String>) {
try {
cmdlineOptions.baseDirectory.createDirectories()
val node = conf.createNode()
val node = conf.createNode(nodeVersionInfo)
node.start()
printPluginsAndServices(node)
@ -116,6 +135,7 @@ fun main(args: Array<String>) {
log.error("Exception during node startup", e)
exitProcess(1)
}
exitProcess(0)
}
@ -154,13 +174,12 @@ private fun messageOfTheDay(): Pair<String, String> {
"Computer science and finance together.\nYou should see our crazy Christmas parties!"
)
if (Emoji.hasEmojiTerminal)
messages +=
"Kind of like a regular database but\nwith emojis, colours and ascii art. ${Emoji.coolGuy}"
messages += "Kind of like a regular database but\nwith emojis, colours and ascii art. ${Emoji.coolGuy}"
val (a, b) = messages.randomOrNull()!!.split('\n')
return Pair(a, b)
}
private fun drawBanner() {
private fun drawBanner(nodeVersionInfo: NodeVersionInfo) {
// This line makes sure ANSI escapes work on Windows, where they aren't supported out of the box.
AnsiConsole.systemInstall()
@ -174,7 +193,7 @@ private fun drawBanner() {
/ / __ / ___/ __ / __ `/ """).fgBrightBlue().a(msg1).newline().fgBrightRed().a(
"/ /___ /_/ / / / /_/ / /_/ / ").fgBrightBlue().a(msg2).newline().fgBrightRed().a(
"""\____/ /_/ \__,_/\__,_/""").reset().newline().newline().fgBrightDefault().bold().
a("--- MILESTONE 9 -------------------------------------------------------------------").
a("--- ${nodeVersionInfo.vendor} ${nodeVersionInfo.version} (${nodeVersionInfo.revision.take(6)}) -----------------------------------------------").
newline().
newline().
a("${Emoji.books}New! ").reset().a("Training now available worldwide, see https://corda.net/corda-training/").

View File

@ -97,6 +97,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
protected abstract val log: Logger
protected abstract val networkMapAddress: SingleMessageRecipient?
protected abstract val version: Version
// We will run as much stuff in this single thread as possible to keep the risk of thread safety bugs low during the
// low-performance prototyping period.
@ -282,7 +283,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
private fun makeInfo(): NodeInfo {
val advertisedServiceEntries = makeServiceEntries()
val legalIdentity = obtainLegalIdentity()
return NodeInfo(net.myAddress, legalIdentity, advertisedServiceEntries, findMyLocation())
return NodeInfo(net.myAddress, legalIdentity, version, advertisedServiceEntries, findMyLocation())
}
/**

View File

@ -4,11 +4,11 @@ import com.codahale.metrics.JmxReporter
import com.google.common.net.HostAndPort
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
import net.corda.core.div
import net.corda.core.flatMap
import net.corda.core.getOrThrow
import net.corda.core.*
import net.corda.core.messaging.RPCOps
import net.corda.core.node.NodeVersionInfo
import net.corda.core.node.ServiceHub
import net.corda.core.node.Version
import net.corda.core.node.services.ServiceInfo
import net.corda.core.node.services.ServiceType
import net.corda.core.node.services.UniquenessProvider
@ -22,14 +22,15 @@ import net.corda.node.services.config.FullNodeConfiguration
import net.corda.node.services.messaging.ArtemisMessagingComponent.NetworkMapAddress
import net.corda.node.services.messaging.ArtemisMessagingServer
import net.corda.node.services.messaging.NodeMessagingClient
import net.corda.node.services.transactions.*
import net.corda.node.services.transactions.PersistentUniquenessProvider
import net.corda.node.services.transactions.RaftUniquenessProvider
import net.corda.node.services.transactions.RaftValidatingNotaryService
import net.corda.node.utilities.AddressUtils
import net.corda.node.utilities.AffinityExecutor
import org.slf4j.Logger
import java.io.RandomAccessFile
import java.lang.management.ManagementFactory
import java.nio.channels.FileLock
import java.nio.file.Files
import java.nio.file.Paths
import java.time.Clock
import javax.management.ObjectName
import kotlin.concurrent.thread
@ -45,8 +46,14 @@ import kotlin.concurrent.thread
*/
class Node(override val configuration: FullNodeConfiguration,
advertisedServices: Set<ServiceInfo>,
val nodeVersionInfo: NodeVersionInfo,
clock: Clock = NodeClock()) : AbstractNode(configuration, advertisedServices, clock) {
override val log = loggerFor<Node>()
companion object {
private val logger = loggerFor<Node>()
}
override val log: Logger get() = logger
override val version: Version get() = nodeVersionInfo.version
override val networkMapAddress: NetworkMapAddress? get() = configuration.networkMapService?.address?.let(::NetworkMapAddress)
// DISCUSSION
@ -103,35 +110,32 @@ class Node(override val configuration: FullNodeConfiguration,
}
/**
* Abort starting the node if an existing deployment with a different version is detected in the current directory.
* The current version is expected to be specified as a system property. If not provided, the check will be ignored.
* Abort starting the node if an existing deployment with a different version is detected in the base directory.
*/
private fun checkVersionUnchanged() {
val currentVersion = System.getProperty("corda.version") ?: return
val versionFile = Paths.get("version")
if (Files.exists(versionFile)) {
val existingVersion = Files.readAllLines(versionFile)[0]
check(existingVersion == currentVersion) {
"Version change detected - current: $currentVersion, existing: $existingVersion. Node upgrades are not yet supported."
val versionFile = configuration.baseDirectory / "version"
if (versionFile.exists()) {
val previousVersion = Version.parse(versionFile.readAllLines()[0])
check(nodeVersionInfo.version.major == previousVersion.major) {
"Major version change detected - current: ${nodeVersionInfo.version}, previous: $previousVersion. " +
"Node upgrades across major versions are not yet supported."
}
} else {
Files.write(versionFile, currentVersion.toByteArray())
}
versionFile.writeLines(listOf(nodeVersionInfo.version.toString()))
}
override fun makeMessagingService(): MessagingServiceInternal {
userService = RPCUserServiceImpl(configuration)
val serverAddress = configuration.messagingServerAddress ?: makeLocalMessageBroker()
val myIdentityOrNullIfNetworkMapService = if (networkMapAddress != null) obtainLegalIdentity().owningKey else null
return NodeMessagingClient(
configuration,
nodeVersionInfo,
serverAddress,
myIdentityOrNullIfNetworkMapService,
serverThread,
database,
networkMapRegistrationFuture
)
networkMapRegistrationFuture)
}
private fun makeLocalMessageBroker(): HostAndPort {

View File

@ -3,6 +3,7 @@ package net.corda.node.services.config
import com.google.common.net.HostAndPort
import com.typesafe.config.Config
import net.corda.core.div
import net.corda.core.node.NodeVersionInfo
import net.corda.core.node.services.ServiceInfo
import net.corda.node.internal.NetworkMapInfo
import net.corda.node.internal.Node
@ -77,7 +78,7 @@ class FullNodeConfiguration(override val baseDirectory: Path, val config: Config
.getListOrElse<String>("notaryClusterAddresses") { emptyList() }
.map { HostAndPort.fromString(it) }
fun createNode(): Node {
fun createNode(nodeVersionInfo: NodeVersionInfo): Node {
// This is a sanity feature do not remove.
require(!useTestClock || devMode) { "Cannot use test clock outside of dev mode" }
@ -87,7 +88,7 @@ class FullNodeConfiguration(override val baseDirectory: Path, val config: Config
.toMutableSet()
if (networkMapService == null) advertisedServices.add(ServiceInfo(NetworkMapService.type))
return Node(this, advertisedServices, if (useTestClock) TestClock() else NodeClock())
return Node(this, advertisedServices, nodeVersionInfo, if (useTestClock) TestClock() else NodeClock())
}
}

View File

@ -5,6 +5,7 @@ import com.google.common.util.concurrent.ListenableFuture
import net.corda.core.ThreadBox
import net.corda.core.crypto.CompositeKey
import net.corda.core.messaging.*
import net.corda.core.node.NodeVersionInfo
import net.corda.core.node.services.PartyInfo
import net.corda.core.serialization.SerializedBytes
import net.corda.core.serialization.opaque
@ -21,6 +22,7 @@ import org.apache.activemq.artemis.api.core.ActiveMQObjectClosedException
import org.apache.activemq.artemis.api.core.Message.*
import org.apache.activemq.artemis.api.core.SimpleString
import org.apache.activemq.artemis.api.core.client.*
import org.apache.activemq.artemis.api.core.client.ActiveMQClient.DEFAULT_ACK_BATCH_SIZE
import org.bouncycastle.asn1.x500.X500Name
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.ResultRow
@ -53,6 +55,7 @@ import javax.annotation.concurrent.ThreadSafe
*/
@ThreadSafe
class NodeMessagingClient(override val config: NodeConfiguration,
nodeVersionInfo: NodeVersionInfo,
val serverHostPort: HostAndPort,
val myIdentity: CompositeKey?,
val nodeExecutor: AffinityExecutor,
@ -65,9 +68,11 @@ class NodeMessagingClient(override val config: NodeConfiguration,
// We should probably try to unify our notion of "topic" (really, just a string that identifies an endpoint
// that will handle messages, like a URL) with the terminology used by underlying MQ libraries, to avoid
// confusion.
const val TOPIC_PROPERTY = "platform-topic"
const val SESSION_ID_PROPERTY = "session-id"
private val AMQ_DELAY: Int = Integer.valueOf(System.getProperty("amq.delivery.delay.ms", "0"))
private val topicProperty = SimpleString("platform-topic")
private val sessionIdProperty = SimpleString("session-id")
private val nodeVersionProperty = SimpleString("node-version")
private val nodeVendorProperty = SimpleString("node-vendor")
private val amqDelay: Int = Integer.valueOf(System.getProperty("amq.delivery.delay.ms", "0"))
}
private class InnerState {
@ -87,6 +92,8 @@ class NodeMessagingClient(override val config: NodeConfiguration,
data class Handler(val topicSession: TopicSession,
val callback: (ReceivedMessage, MessageHandlerRegistration) -> Unit) : MessageHandlerRegistration
private val nodeVendor = SimpleString(nodeVersionInfo.vendor)
private val version = SimpleString(nodeVersionInfo.version.toString())
/** An executor for sending messages */
private val messagingExecutor = AffinityExecutor.ServiceAffinityExecutor("Messaging", 1)
@ -130,7 +137,7 @@ class NodeMessagingClient(override val config: NodeConfiguration,
// using our TLS certificate.
// Note that the acknowledgement of messages is not flushed to the Artermis journal until the default buffer
// size of 1MB is acknowledged.
val session = clientFactory!!.createSession(NODE_USER, NODE_USER, false, true, true, locator.isPreAcknowledge, ActiveMQClient.DEFAULT_ACK_BATCH_SIZE)
val session = clientFactory!!.createSession(NODE_USER, NODE_USER, false, true, true, locator.isPreAcknowledge, DEFAULT_ACK_BATCH_SIZE)
this.session = session
session.start()
@ -165,7 +172,7 @@ class NodeMessagingClient(override val config: NodeConfiguration,
private fun makeP2PConsumer(session: ClientSession, networkMapOnly: Boolean): ClientConsumer {
return if (networkMapOnly) {
// Filter for just the network map messages.
val messageFilter = "hyphenated_props:$TOPIC_PROPERTY like 'platform.network_map.%'"
val messageFilter = "hyphenated_props:$topicProperty like 'platform.network_map.%'"
session.createConsumer(P2P_QUEUE, messageFilter)
} else
session.createConsumer(P2P_QUEUE)
@ -249,18 +256,10 @@ class NodeMessagingClient(override val config: NodeConfiguration,
private fun artemisToCordaMessage(message: ClientMessage): ReceivedMessage? {
try {
if (!message.containsProperty(TOPIC_PROPERTY)) {
log.warn("Received message without a $TOPIC_PROPERTY property, ignoring")
return null
}
if (!message.containsProperty(SESSION_ID_PROPERTY)) {
log.warn("Received message without a $SESSION_ID_PROPERTY property, ignoring")
return null
}
val topic = message.getStringProperty(TOPIC_PROPERTY)
val sessionID = message.getLongProperty(SESSION_ID_PROPERTY)
val topic = message.required(topicProperty) { getStringProperty(it) }
val sessionID = message.required(sessionIdProperty) { getLongProperty(it) }
// 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 = message.required(HDR_DUPLICATE_DETECTION_ID) { UUID.fromString(message.getStringProperty(it)) }
val user = requireNotNull(message.getStringProperty(HDR_VALIDATED_USER)) { "Message is not authenticated" }
log.trace { "Received message from: ${message.address} user: $user topic: $topic sessionID: $sessionID uuid: $uuid" }
@ -277,11 +276,16 @@ class NodeMessagingClient(override val config: NodeConfiguration,
return msg
} catch (e: Exception) {
log.error("Internal error whilst reading MQ message", e)
log.error("Unable to process message, ignoring it: $message", e)
return null
}
}
private inline fun <T> ClientMessage.required(key: SimpleString, extractor: ClientMessage.(SimpleString) -> T): T {
require(containsProperty(key)) { "Missing $key" }
return extractor(key)
}
private fun deliver(msg: ReceivedMessage): Boolean {
state.checkNotLocked()
// Because handlers is a COW list, the loop inside filter will operate on a snapshot. Handlers being added
@ -368,16 +372,17 @@ class NodeMessagingClient(override val config: NodeConfiguration,
state.locked {
val mqAddress = getMQAddress(target)
val artemisMessage = session!!.createMessage(true).apply {
val sessionID = message.topicSession.sessionID
putStringProperty(TOPIC_PROPERTY, message.topicSession.topic)
putLongProperty(SESSION_ID_PROPERTY, sessionID)
putStringProperty(nodeVendorProperty, nodeVendor)
putStringProperty(nodeVersionProperty, version)
putStringProperty(topicProperty, SimpleString(message.topicSession.topic))
putLongProperty(sessionIdProperty, message.topicSession.sessionID)
writeBodyBufferBytes(message.data)
// Use the magic deduplication property built into Artemis as our message identity too
putStringProperty(HDR_DUPLICATE_DETECTION_ID, SimpleString(message.uniqueMessageId.toString()))
// 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) {
putLongProperty(HDR_SCHEDULED_DELIVERY_TIME, System.currentTimeMillis() + AMQ_DELAY)
if (amqDelay > 0 && message.topicSession.topic == StateMachineManager.sessionTopic.topic) {
putLongProperty(HDR_SCHEDULED_DELIVERY_TIME, System.currentTimeMillis() + amqDelay)
}
}
log.trace { "Send to: $mqAddress topic: ${message.topicSession.topic} " +
@ -387,7 +392,6 @@ class NodeMessagingClient(override val config: NodeConfiguration,
}
}
private fun getMQAddress(target: MessageRecipients): String {
return if (target == myAddress) {
// If we are sending to ourselves then route the message directly to our P2P queue.

View File

@ -215,7 +215,7 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
state = FlowSessionState.Initiated(peerParty, sessionInitResponse.initiatedSessionId)
} else {
sessionInitResponse as SessionReject
throw FlowException("Party ${state.sendToParty} rejected session request: ${sessionInitResponse.errorMessage}")
throw FlowSessionException("Party ${state.sendToParty} rejected session request: ${sessionInitResponse.errorMessage}")
}
}

View File

@ -20,7 +20,8 @@ class ArgsParserTest {
help = false,
logToConsole = false,
loggingLevel = Level.INFO,
isRegistration = false))
isRegistration = false,
isVersion = false))
}
@Test
@ -54,20 +55,6 @@ class ArgsParserTest {
assertThat(cmdLineOptions.configFile).isEqualTo(configFile)
}
@Test
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 `both base-directory and config-file`() {
assertThatExceptionOfType(IllegalArgumentException::class.java).isThrownBy {
@ -89,6 +76,20 @@ class ArgsParserTest {
}.withMessageContaining("config-file")
}
@Test
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 `logging-level without argument`() {
assertThatExceptionOfType(OptionException::class.java).isThrownBy {
@ -102,4 +103,16 @@ class ArgsParserTest {
parser.parse("--logging-level", "not-a-level")
}.withMessageContaining("logging-level")
}
@Test
fun `initial-registration`() {
val cmdLineOptions = parser.parse("--initial-registration")
assertThat(cmdLineOptions.isRegistration).isTrue()
}
@Test
fun version() {
val cmdLineOptions = parser.parse("--version")
assertThat(cmdLineOptions.isVersion).isTrue()
}
}

View File

@ -23,6 +23,7 @@ import net.corda.node.services.transactions.PersistentUniquenessProvider
import net.corda.node.utilities.AffinityExecutor.ServiceAffinityExecutor
import net.corda.node.utilities.configureDatabase
import net.corda.node.utilities.databaseTransaction
import net.corda.testing.MOCK_NODE_VERSION_INFO
import net.corda.testing.TestNodeConfiguration
import net.corda.testing.freeLocalHostAndPort
import net.corda.testing.node.makeTestDataSourceProperties
@ -219,6 +220,7 @@ class ArtemisMessagingTests {
return databaseTransaction(database) {
NodeMessagingClient(
config,
MOCK_NODE_VERSION_INFO,
server,
identity.public.composite,
ServiceAffinityExecutor("ArtemisMessagingTests", 1),