From 0df77d5b64aaae532b206f8a1e0aff1634b7a4e4 Mon Sep 17 00:00:00 2001 From: Michele Sollecito Date: Mon, 3 Sep 2018 16:15:03 +0100 Subject: [PATCH] Backports: CORDA-1663, CORDA-1638, CORDA-1542 (#3881) --- docs/source/changelog.rst | 7 +++ ...gratedAttachmentContractsTableNameTests.kt | 14 +++--- .../net/corda/node/internal/NodeStartup.kt | 4 ++ .../node/services/vault/NodeVaultService.kt | 47 +++++++++++++------ .../net/corda/node/shell/InteractiveShell.kt | 18 ++++--- .../HTTPNetworkRegistrationService.kt | 3 ++ .../registration/NetworkRegistrationHelper.kt | 38 +++++++++++++-- 7 files changed, 98 insertions(+), 33 deletions(-) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 962f80736a..63854dda4a 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -6,6 +6,13 @@ release, see :doc:`upgrade-notes`. Unreleased ========== +* Fixed an issue preventing Shell from returning control to the user when CTRL+C is pressed in the terminal. + +* Fixed a problem that sometimes prevented nodes from starting in presence of custom state types in the database without a corresponding type from installed CorDapps. + +* Introduced a grace period before the initial node registration fails if the node cannot connect to the Doorman. + It retries 10 times with a 1 minute interval in between each try. At the moment this is not configurable. + * Fixed an error thrown by NodeVaultService upon recording a transaction with a number of inputs greater than the default page size. * Changes to the JSON/YAML serialisation format from ``JacksonSupport``, which also applies to the node shell: diff --git a/node/src/integration-test/kotlin/net/corda/node/persistence/FailNodeOnNotMigratedAttachmentContractsTableNameTests.kt b/node/src/integration-test/kotlin/net/corda/node/persistence/FailNodeOnNotMigratedAttachmentContractsTableNameTests.kt index 22bcdc872a..bcc34f2744 100644 --- a/node/src/integration-test/kotlin/net/corda/node/persistence/FailNodeOnNotMigratedAttachmentContractsTableNameTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/persistence/FailNodeOnNotMigratedAttachmentContractsTableNameTests.kt @@ -5,6 +5,7 @@ import net.corda.core.internal.packageName import net.corda.core.messaging.startFlow import net.corda.core.utilities.getOrThrow import net.corda.node.services.Permissions +import net.corda.nodeapi.internal.persistence.DatabaseIncompatibleException import net.corda.test.node.Message import net.corda.test.node.MessageState import net.corda.test.node.SendMessageFlow @@ -13,6 +14,8 @@ import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.driver import net.corda.testing.driver.internal.RandomFree import net.corda.testing.node.User +import org.assertj.core.api.Assertions +import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.Test import java.nio.file.Path import java.sql.DriverManager @@ -32,9 +35,9 @@ class FailNodeOnNotMigratedAttachmentContractsTableNameTests { fun `node fails when not detecting compatible table name`(tableNameFromMapping: String, tableNameInDB: String) { val user = User("mark", "dadada", setOf(Permissions.startFlow(), Permissions.invokeRpc("vaultQuery"))) val message = Message("Hello world!") - val baseDir: Path = driver(DriverParameters(startNodesInProcess = true, - portAllocation = RandomFree, extraCordappPackagesToScan = listOf(MessageState::class.packageName))) { + val baseDir: Path = driver(DriverParameters(startNodesInProcess = true, portAllocation = RandomFree, extraCordappPackagesToScan = listOf(MessageState::class.packageName))) { val (nodeName, baseDir) = { + defaultNotaryNode.getOrThrow() val nodeHandle = startNode(rpcUsers = listOf(user)).getOrThrow() val nodeName = nodeHandle.nodeInfo.singleIdentity().name CordaRPCClient(nodeHandle.rpcAddress).start(user.username, user.password).use { @@ -49,11 +52,8 @@ class FailNodeOnNotMigratedAttachmentContractsTableNameTests { it.createStatement().execute("ALTER TABLE $tableNameFromMapping RENAME TO $tableNameInDB") it.commit() } - assertFailsWith(net.corda.nodeapi.internal.persistence.DatabaseIncompatibleException::class) { - val nodeHandle = startNode(providedName = nodeName, rpcUsers = listOf(user)).getOrThrow() - nodeHandle.stop() - } - baseDir + assertThatThrownBy { startNode(providedName = nodeName, rpcUsers = listOf(user)).getOrThrow() }.isInstanceOf(DatabaseIncompatibleException::class.java) + baseDir } // check that the node didn't recreated the correct table matching it's entity mapping diff --git a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt index 859e26db08..02e4d8a458 100644 --- a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt +++ b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt @@ -16,6 +16,7 @@ import net.corda.node.services.transactions.bftSMaRtSerialFilter import net.corda.node.shell.InteractiveShell import net.corda.node.utilities.registration.HTTPNetworkRegistrationService import net.corda.node.utilities.registration.NetworkRegistrationHelper +import net.corda.node.utilities.registration.UnableToRegisterNodeWithDoormanException import net.corda.nodeapi.internal.addShutdownHook import net.corda.nodeapi.internal.persistence.DatabaseIncompatibleException import org.fusesource.jansi.Ansi @@ -106,6 +107,9 @@ open class NodeStartup(val args: Array) { return true } logStartupInfo(versionInfo, cmdlineOptions, conf) + } catch (e: UnableToRegisterNodeWithDoormanException) { + logger.warn("Node registration service is unavailable. Perhaps try to perform the initial registration again after a while.") + return false } catch (e: Exception) { logger.error("Exception during node registration", e) return false diff --git a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt index 19cb690aa5..bc1e4eaa7d 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt @@ -380,8 +380,8 @@ class NodeVaultService( log.trace { "State update of type: $concreteType" } val seen = contractStateTypeMappings.any { it.value.contains(concreteType.name) } if (!seen) { - val contractInterfaces = deriveContractInterfaces(concreteType) - contractInterfaces.map { + val contractStateTypes = deriveContractTypes(concreteType) + contractStateTypes.map { val contractInterface = contractStateTypeMappings.getOrPut(it.name, { mutableSetOf() }) contractInterface.add(concreteType.name) } @@ -490,25 +490,42 @@ class NodeVaultService( val distinctTypes = results.map { it } val contractInterfaceToConcreteTypes = mutableMapOf>() + val unknownTypes = mutableSetOf() distinctTypes.forEach { type -> - val concreteType: Class = uncheckedCast(Class.forName(type)) - val contractInterfaces = deriveContractInterfaces(concreteType) - contractInterfaces.map { - val contractInterface = contractInterfaceToConcreteTypes.getOrPut(it.name, { mutableSetOf() }) - contractInterface.add(concreteType.name) + val concreteType: Class? = try { + uncheckedCast(Class.forName(type)) + } catch (e: ClassNotFoundException) { + unknownTypes += type + null } + concreteType?.let { + val contractTypes = deriveContractTypes(it) + contractTypes.map { + val contractStateType = contractInterfaceToConcreteTypes.getOrPut(it.name) { mutableSetOf() } + contractStateType.add(concreteType.name) + } + } + } + if (unknownTypes.isNotEmpty()) { + log.warn("There are unknown contract state types in the vault, which will prevent these states from being used. The relevant CorDapps must be loaded for these states to be used. The types not on the classpath are ${unknownTypes.joinToString(", ", "[", "]")}.") } return contractInterfaceToConcreteTypes } - private fun deriveContractInterfaces(clazz: Class): Set> { - val myInterfaces: MutableSet> = mutableSetOf() - clazz.interfaces.forEach { - if (it != ContractState::class.java) { - myInterfaces.add(uncheckedCast(it)) - myInterfaces.addAll(deriveContractInterfaces(uncheckedCast(it))) + private fun deriveContractTypes(clazz: Class): Set> { + val myTypes : MutableSet> = mutableSetOf() + clazz.superclass?.let { + if (!it.isInstance(Any::class)) { + myTypes.add(uncheckedCast(it)) + myTypes.addAll(deriveContractTypes(uncheckedCast(it))) } } - return myInterfaces + clazz.interfaces.forEach { + if (it != ContractState::class.java) { + myTypes.add(uncheckedCast(it)) + myTypes.addAll(deriveContractTypes(uncheckedCast(it))) + } + } + return myTypes } -} +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/shell/InteractiveShell.kt b/node/src/main/kotlin/net/corda/node/shell/InteractiveShell.kt index 52b683b150..d13997d2c3 100644 --- a/node/src/main/kotlin/net/corda/node/shell/InteractiveShell.kt +++ b/node/src/main/kotlin/net/corda/node/shell/InteractiveShell.kt @@ -240,12 +240,18 @@ object InteractiveShell { val latch = CountDownLatch(1) ansiProgressRenderer.render(stateObservable, { latch.countDown() }) - try { - // Wait for the flow to end and the progress tracker to notice. By the time the latch is released - // the tracker is done with the screen. - latch.await() - } catch (e: InterruptedException) { - // TODO: When the flow framework allows us to kill flows mid-flight, do so here. + while (!Thread.currentThread().isInterrupted) { + try { + latch.await() + break + } catch (e: InterruptedException) { + try { + // TODO: When the flow framework allows us to kill flows mid-flight, do so here. + } finally { + Thread.currentThread().interrupt() + break + } + } } stateObservable.returnValue.get()?.apply { if (this !is Throwable) { diff --git a/node/src/main/kotlin/net/corda/node/utilities/registration/HTTPNetworkRegistrationService.kt b/node/src/main/kotlin/net/corda/node/utilities/registration/HTTPNetworkRegistrationService.kt index 7f3d0ddf24..33b7b6b7d7 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/registration/HTTPNetworkRegistrationService.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/registration/HTTPNetworkRegistrationService.kt @@ -15,6 +15,7 @@ import java.net.URL import java.security.cert.X509Certificate import java.util.* import java.util.zip.ZipInputStream +import javax.naming.ServiceUnavailableException class HTTPNetworkRegistrationService(compatibilityZoneURL: URL) : NetworkRegistrationService { private val registrationURL = URL("$compatibilityZoneURL/certificate") @@ -22,6 +23,7 @@ class HTTPNetworkRegistrationService(compatibilityZoneURL: URL) : NetworkRegistr companion object { // TODO: Propagate version information from gradle val clientVersion = "1.0" + private val TRANSIENT_ERROR_STATUS_CODES = setOf(HTTP_BAD_GATEWAY, HTTP_UNAVAILABLE, HTTP_GATEWAY_TIMEOUT) } @Throws(CertificateRequestException::class) @@ -44,6 +46,7 @@ class HTTPNetworkRegistrationService(compatibilityZoneURL: URL) : NetworkRegistr } HTTP_NO_CONTENT -> CertificateResponse(pollInterval, null) HTTP_UNAUTHORIZED -> throw CertificateRequestException("Certificate signing request has been rejected: ${conn.errorMessage}") + in TRANSIENT_ERROR_STATUS_CODES -> throw ServiceUnavailableException("Could not connect with Doorman. Http response status code was ${conn.responseCode}.") else -> throwUnexpectedResponseCode(conn) } } diff --git a/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt b/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt index 1a253a701a..ac8fc47014 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt @@ -15,11 +15,14 @@ import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_CLIENT_TLS import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_ROOT_CA import org.bouncycastle.openssl.jcajce.JcaPEMWriter import org.bouncycastle.util.io.pem.PemObject +import java.io.IOException import java.io.StringWriter import java.nio.file.Path import java.security.KeyPair import java.security.KeyStore import java.security.cert.X509Certificate +import java.time.Duration +import javax.naming.ServiceUnavailableException /** * Helper for managing the node registration process, which checks for any existing certificates and requests them if @@ -31,7 +34,8 @@ class NetworkRegistrationHelper(private val config: SSLConfiguration, private val certService: NetworkRegistrationService, private val networkRootTrustStorePath: Path, networkRootTrustStorePassword: String, - private val certRole: CertRole) { + private val certRole: CertRole, + private val nextIdleDuration: (Duration?) -> Duration? = FixedPeriodLimitedRetrialStrategy(10, Duration.ofMinutes(1))) { // Constructor for corda node, cert role is restricted to [CertRole.NODE_CA]. constructor(config: NodeConfiguration, certService: NetworkRegistrationService, regConfig: NodeRegistrationOption) : @@ -171,12 +175,22 @@ class NetworkRegistrationHelper(private val config: SSLConfiguration, private fun pollServerForCertificates(requestId: String): List { println("Start polling server for certificate signing approval.") // Poll server to download the signed certificate once request has been approved. + var idlePeriodDuration: Duration? = null while (true) { - val (pollInterval, certificates) = certService.retrieveCertificates(requestId) - if (certificates != null) { - return certificates + try { + val (pollInterval, certificates) = certService.retrieveCertificates(requestId) + if (certificates != null) { + return certificates + } + Thread.sleep(pollInterval.toMillis()) + } catch (e: ServiceUnavailableException) { + idlePeriodDuration = nextIdleDuration(idlePeriodDuration) + if (idlePeriodDuration != null) { + Thread.sleep(idlePeriodDuration.toMillis()) + } else { + throw UnableToRegisterNodeWithDoormanException() + } } - Thread.sleep(pollInterval.toMillis()) } } @@ -216,3 +230,17 @@ class NetworkRegistrationHelper(private val config: SSLConfiguration, } } } + +class UnableToRegisterNodeWithDoormanException : IOException() + +private class FixedPeriodLimitedRetrialStrategy(times: Int, private val period: Duration) : (Duration?) -> Duration? { + init { + require(times > 0) + } + private var counter = times + override fun invoke(@Suppress("UNUSED_PARAMETER") previousPeriod: Duration?): Duration? { + synchronized(this) { + return if (counter-- > 0) period else null + } + } +} \ No newline at end of file