From 70f1ea0a9d0224312a3896df94389ae387995fb2 Mon Sep 17 00:00:00 2001 From: Christian Sailer Date: Fri, 22 May 2020 16:27:10 +0100 Subject: [PATCH 01/45] ENT-5258 db schema set-up only via command line flag (#6280) Removing the ability to initialise schema from the node config, and add a new sub-command to initialise the schema (that does not do anything else and exits afterwards). Also adding a command line flag that allow app schema to be maintained by hibernate for legacy cordapps, tests or rapid development. Patching up mock net and driver test frameworks so they create the required schemas for tests to work, defaulting schema migration and hibernate schema management to true to match pre-existing behaviour. Modified network bootstrapper to run an initial schema set-up so it can register nodes. --- .../net/corda/common/logging/Constants.kt | 2 +- .../corda/coretests/flows/FlowIsKilledTest.kt | 4 +- .../persistence/MissingSchemaMigrationTest.kt | 14 +- .../internal/network/NetworkBootstrapper.kt | 55 ++-- .../internal/persistence/CordaPersistence.kt | 25 +- .../persistence/HibernateConfiguration.kt | 5 +- .../internal/persistence/SchemaMigration.kt | 24 +- .../factory/BaseSessionFactoryFactory.kt | 18 +- .../factory/CordaSessionFactoryFactory.kt | 3 +- ...urnFailureReproductionIntegrationTest.java | 1 + .../kotlin/net/corda/node/BootTests.kt | 2 +- .../net/corda/node/NodeKeystoreCheckTest.kt | 4 +- .../persistence/DbSchemaInitialisationTest.kt | 21 +- .../node/services/network/NetworkMapTest.kt | 9 +- .../services/rpc/RpcExceptionHandlingTest.kt | 6 +- .../vault/VaultObserverExceptionTest.kt | 82 +++--- .../net/corda/node/NodeCmdLineOptions.kt | 8 + .../net/corda/node/internal/AbstractNode.kt | 93 +++++-- .../kotlin/net/corda/node/internal/Node.kt | 13 +- .../net/corda/node/internal/NodeStartup.kt | 10 +- .../subcommands/RunMigrationScriptsCli.kt | 16 ++ .../services/config/NodeConfigurationImpl.kt | 3 - .../config/schema/v1/ConfigSections.kt | 26 +- node/src/main/resources/reference.conf | 1 - .../services/persistence/DbMapDeadlockTest.kt | 5 +- .../persistence/HibernateConfigurationTest.kt | 3 +- .../test-config-quasarexcludepackages.conf | 1 - node/src/test/resources/working-config.conf | 1 - .../kotlin/net/corda/testing/driver/Driver.kt | 7 +- .../testing/node/internal/DriverDSLImpl.kt | 238 ++++++++++-------- .../node/internal/InternalMockNetwork.kt | 5 +- .../testing/node/internal/NodeBasedTest.kt | 7 +- .../testing/internal/InternalTestUtils.kt | 17 +- 33 files changed, 438 insertions(+), 291 deletions(-) create mode 100644 node/src/main/kotlin/net/corda/node/internal/subcommands/RunMigrationScriptsCli.kt diff --git a/common/logging/src/main/kotlin/net/corda/common/logging/Constants.kt b/common/logging/src/main/kotlin/net/corda/common/logging/Constants.kt index b08dda81d7..5eb0817584 100644 --- a/common/logging/src/main/kotlin/net/corda/common/logging/Constants.kt +++ b/common/logging/src/main/kotlin/net/corda/common/logging/Constants.kt @@ -9,4 +9,4 @@ package net.corda.common.logging * (originally added to source control for ease of use) */ -internal const val CURRENT_MAJOR_RELEASE = "4.6-SNAPSHOT" +internal const val CURRENT_MAJOR_RELEASE = "4.6-SNAPSHOT" \ No newline at end of file diff --git a/core-tests/src/test/kotlin/net/corda/coretests/flows/FlowIsKilledTest.kt b/core-tests/src/test/kotlin/net/corda/coretests/flows/FlowIsKilledTest.kt index 14a2607b26..60781aad72 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/flows/FlowIsKilledTest.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/flows/FlowIsKilledTest.kt @@ -112,7 +112,7 @@ class FlowIsKilledTest { } @Test(timeout = 300_000) - fun `manually handle killed flows using checkFlowIsNotKilled`() { + fun `manually handle killed flows using checkForIsNotKilled`() { driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { val alice = startNode(providedName = ALICE_NAME).getOrThrow() alice.rpc.let { rpc -> @@ -131,7 +131,7 @@ class FlowIsKilledTest { } @Test(timeout = 300_000) - fun `manually handle killed flows using checkFlowIsNotKilled with lazy message`() { + fun `manually handle killed flows using checkForIsNotKilled with lazy message`() { driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { val alice = startNode(providedName = ALICE_NAME).getOrThrow() alice.rpc.let { rpc -> diff --git a/node-api-tests/src/test/kotlin/net/corda/nodeapitests/internal/persistence/MissingSchemaMigrationTest.kt b/node-api-tests/src/test/kotlin/net/corda/nodeapitests/internal/persistence/MissingSchemaMigrationTest.kt index b81a0eaceb..05891254be 100644 --- a/node-api-tests/src/test/kotlin/net/corda/nodeapitests/internal/persistence/MissingSchemaMigrationTest.kt +++ b/node-api-tests/src/test/kotlin/net/corda/nodeapitests/internal/persistence/MissingSchemaMigrationTest.kt @@ -2,12 +2,11 @@ package net.corda.nodeapitests.internal.persistence import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentState -import net.corda.nodeapi.internal.persistence.DatabaseConfig -import net.corda.nodeapi.internal.persistence.MissingMigrationException -import net.corda.nodeapi.internal.persistence.SchemaMigration import net.corda.node.internal.DataSourceFactory import net.corda.node.services.persistence.DBCheckpointStorage import net.corda.node.services.schema.NodeSchemaService +import net.corda.nodeapi.internal.persistence.MissingMigrationException +import net.corda.nodeapi.internal.persistence.SchemaMigration import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.TestIdentity import net.corda.testing.node.MockServices @@ -41,8 +40,7 @@ class MissingSchemaMigrationTest { } private fun createSchemaMigration(schemasToMigrate: Set, forceThrowOnMissingMigration: Boolean): SchemaMigration { - val databaseConfig = DatabaseConfig() - return SchemaMigration(schemasToMigrate, dataSource, databaseConfig, null, null, + return SchemaMigration(schemasToMigrate, dataSource, null, null, TestIdentity(ALICE_NAME, 70).name, forceThrowOnMissingMigration) } @@ -50,7 +48,7 @@ class MissingSchemaMigrationTest { fun `test that an error is thrown when forceThrowOnMissingMigration is set and a mapped schema is missing a migration`() { assertThatThrownBy { createSchemaMigration(setOf(GoodSchema), true) - .nodeStartup(dataSource.connection.use { DBCheckpointStorage().getCheckpointCount(it) != 0L }) + .runMigration(dataSource.connection.use { DBCheckpointStorage().getCheckpointCount(it) != 0L }) }.isInstanceOf(MissingMigrationException::class.java) } @@ -58,7 +56,7 @@ class MissingSchemaMigrationTest { fun `test that an error is not thrown when forceThrowOnMissingMigration is not set and a mapped schema is missing a migration`() { assertDoesNotThrow { createSchemaMigration(setOf(GoodSchema), false) - .nodeStartup(dataSource.connection.use { DBCheckpointStorage().getCheckpointCount(it) != 0L }) + .runMigration(dataSource.connection.use { DBCheckpointStorage().getCheckpointCount(it) != 0L }) } } @@ -67,7 +65,7 @@ class MissingSchemaMigrationTest { assertDoesNotThrow("This test failure indicates " + "a new table has been added to the node without the appropriate migration scripts being present") { createSchemaMigration(NodeSchemaService().internalSchemas(), false) - .nodeStartup(dataSource.connection.use { DBCheckpointStorage().getCheckpointCount(it) != 0L }) + .runMigration(dataSource.connection.use { DBCheckpointStorage().getCheckpointCount(it) != 0L }) } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt index d2f8da84c3..3ec783ec99 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt @@ -75,6 +75,13 @@ constructor(private val initSerEnv: Boolean, "generate-node-info" ) + private val createSchemasCmd = listOf( + Paths.get(System.getProperty("java.home"), "bin", "java").toString(), + "-jar", + "corda.jar", + "run-migration-scripts" + ) + private const val LOGS_DIR_NAME = "logs" private val jarsThatArentCordapps = setOf("corda.jar", "runnodes.jar") @@ -92,7 +99,9 @@ constructor(private val initSerEnv: Boolean, } val executor = Executors.newFixedThreadPool(numParallelProcesses) return try { - nodeDirs.map { executor.fork { generateNodeInfo(it) } }.transpose().getOrThrow() + nodeDirs.map { executor.fork { + createDbSchemas(it) + generateNodeInfo(it) } }.transpose().getOrThrow() } finally { warningTimer.cancel() executor.shutdownNow() @@ -100,23 +109,31 @@ constructor(private val initSerEnv: Boolean, } private fun generateNodeInfo(nodeDir: Path): Path { + runNodeJob(nodeInfoGenCmd, nodeDir, "node-info-gen.log") + return nodeDir.list { paths -> + paths.filter { it.fileName.toString().startsWith(NODE_INFO_FILE_NAME_PREFIX) }.findFirst().get() + } + } + + private fun createDbSchemas(nodeDir: Path) { + runNodeJob(createSchemasCmd, nodeDir, "node-run-migration.log") + } + + private fun runNodeJob(command: List, nodeDir: Path, logfileName: String) { val logsDir = (nodeDir / LOGS_DIR_NAME).createDirectories() - val nodeInfoGenFile = (logsDir / "node-info-gen.log").toFile() - val process = ProcessBuilder(nodeInfoGenCmd) + val nodeRedirectFile = (logsDir / logfileName).toFile() + val process = ProcessBuilder(command) .directory(nodeDir.toFile()) .redirectErrorStream(true) - .redirectOutput(nodeInfoGenFile) + .redirectOutput(nodeRedirectFile) .apply { environment()["CAPSULE_CACHE_DIR"] = "../.cache" } .start() try { if (!process.waitFor(3, TimeUnit.MINUTES)) { process.destroyForcibly() - printNodeInfoGenLogToConsole(nodeInfoGenFile) - } - printNodeInfoGenLogToConsole(nodeInfoGenFile) { process.exitValue() == 0 } - return nodeDir.list { paths -> - paths.filter { it.fileName.toString().startsWith(NODE_INFO_FILE_NAME_PREFIX) }.findFirst().get() + printNodeOutputToConsoleAndThrow(nodeRedirectFile) } + if (process.exitValue() != 0) printNodeOutputToConsoleAndThrow(nodeRedirectFile) } catch (e: InterruptedException) { // Don't leave this process dangling if the thread is interrupted. process.destroyForcibly() @@ -124,18 +141,16 @@ constructor(private val initSerEnv: Boolean, } } - private fun printNodeInfoGenLogToConsole(nodeInfoGenFile: File, check: (() -> Boolean) = { true }) { - if (!check.invoke()) { - val nodeDir = nodeInfoGenFile.parent - val nodeIdentifier = try { - ConfigFactory.parseFile((nodeDir / "node.conf").toFile()).getString("myLegalName") - } catch (e: ConfigException) { - nodeDir - } - System.err.println("#### Error while generating node info file $nodeIdentifier ####") - nodeInfoGenFile.inputStream().copyTo(System.err) - throw IllegalStateException("Error while generating node info file. Please check the logs in $nodeDir.") + private fun printNodeOutputToConsoleAndThrow(stdoutFile: File) { + val nodeDir = stdoutFile.parent + val nodeIdentifier = try { + ConfigFactory.parseFile((nodeDir / "node.conf").toFile()).getString("myLegalName") + } catch (e: ConfigException) { + nodeDir } + System.err.println("#### Error while generating node info file $nodeIdentifier ####") + stdoutFile.inputStream().copyTo(System.err) + throw IllegalStateException("Error while generating node info file. Please check the logs in $nodeDir.") } const val DEFAULT_MAX_MESSAGE_SIZE: Int = 10485760 diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/CordaPersistence.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/CordaPersistence.kt index d54575102b..0535f7a8aa 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/CordaPersistence.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/CordaPersistence.kt @@ -31,24 +31,12 @@ import javax.sql.DataSource */ const val NODE_DATABASE_PREFIX = "node_" -enum class SchemaInitializationType{ - NONE, - VALIDATE, - UPDATE -} - // This class forms part of the node config and so any changes to it must be handled with care data class DatabaseConfig( - val initialiseSchema: Boolean = Defaults.initialiseSchema, - val initialiseAppSchema: SchemaInitializationType = Defaults.initialiseAppSchema, - val transactionIsolationLevel: TransactionIsolationLevel = Defaults.transactionIsolationLevel, val exportHibernateJMXStatistics: Boolean = Defaults.exportHibernateJMXStatistics, val mappedSchemaCacheSize: Long = Defaults.mappedSchemaCacheSize ) { object Defaults { - val initialiseSchema = true - val initialiseAppSchema = SchemaInitializationType.UPDATE - val transactionIsolationLevel = TransactionIsolationLevel.REPEATABLE_READ val exportHibernateJMXStatistics = false val mappedSchemaCacheSize = 100L } @@ -67,6 +55,10 @@ enum class TransactionIsolationLevel { */ val jdbcString = "TRANSACTION_$name" val jdbcValue: Int = java.sql.Connection::class.java.getField(jdbcString).get(null) as Int + + companion object{ + val default = REPEATABLE_READ + } } internal val _prohibitDatabaseAccess = ThreadLocal.withInitial { false } @@ -103,20 +95,21 @@ class CordaPersistence( attributeConverters: Collection> = emptySet(), customClassLoader: ClassLoader? = null, val closeConnection: Boolean = true, - val errorHandler: DatabaseTransaction.(e: Exception) -> Unit = {} + val errorHandler: DatabaseTransaction.(e: Exception) -> Unit = {}, + allowHibernateToManageAppSchema: Boolean = false ) : Closeable { companion object { private val log = contextLogger() } - private val defaultIsolationLevel = databaseConfig.transactionIsolationLevel + private val defaultIsolationLevel = TransactionIsolationLevel.default val hibernateConfig: HibernateConfiguration by lazy { transaction { try { - HibernateConfiguration(schemas, databaseConfig, attributeConverters, jdbcUrl, cacheFactory, customClassLoader) + HibernateConfiguration(schemas, databaseConfig, attributeConverters, jdbcUrl, cacheFactory, customClassLoader, allowHibernateToManageAppSchema) } catch (e: Exception) { when (e) { - is SchemaManagementException -> throw HibernateSchemaChangeException("Incompatible schema change detected. Please run the node with database.initialiseSchema=true. Reason: ${e.message}", e) + is SchemaManagementException -> throw HibernateSchemaChangeException("Incompatible schema change detected. Please run schema migration scripts (node with sub-command run-migration-scripts). Reason: ${e.message}", e) else -> throw HibernateConfigException("Could not create Hibernate configuration: ${e.message}", e) } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/HibernateConfiguration.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/HibernateConfiguration.kt index 01bd86b7d1..cfa325d5fc 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/HibernateConfiguration.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/HibernateConfiguration.kt @@ -23,7 +23,8 @@ class HibernateConfiguration( private val attributeConverters: Collection>, jdbcUrl: String, cacheFactory: NamedCacheFactory, - val customClassLoader: ClassLoader? = null + val customClassLoader: ClassLoader? = null, + val allowHibernateToManageAppSchema: Boolean = false ) { companion object { private val logger = contextLogger() @@ -64,7 +65,7 @@ class HibernateConfiguration( fun sessionFactoryForSchemas(key: Set): SessionFactory = sessionFactories.get(key, ::makeSessionFactoryForSchemas)!! private fun makeSessionFactoryForSchemas(schemas: Set): SessionFactory { - val sessionFactory = sessionFactoryFactory.makeSessionFactoryForSchemas(databaseConfig, schemas, customClassLoader, attributeConverters) + val sessionFactory = sessionFactoryFactory.makeSessionFactoryForSchemas(databaseConfig, schemas, customClassLoader, attributeConverters, allowHibernateToManageAppSchema) // export Hibernate JMX statistics if (databaseConfig.exportHibernateJMXStatistics) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/SchemaMigration.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/SchemaMigration.kt index 47a8f5a801..ce592d95da 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/SchemaMigration.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/SchemaMigration.kt @@ -25,7 +25,6 @@ import kotlin.concurrent.withLock class SchemaMigration( val schemas: Set, val dataSource: DataSource, - private val databaseConfig: DatabaseConfig, cordappLoader: CordappLoader? = null, private val currentDirectory: Path?, // This parameter is used by the vault state migration to establish what the node's legal identity is when setting up @@ -50,29 +49,18 @@ class SchemaMigration( private val classLoader = cordappLoader?.appClassLoader ?: Thread.currentThread().contextClassLoader - /** - * Main entry point to the schema migration. - * Called during node startup. - */ - fun nodeStartup(existingCheckpoints: Boolean) { - when { - databaseConfig.initialiseSchema -> { - migrateOlderDatabaseToUseLiquibase(existingCheckpoints) - runMigration(existingCheckpoints) - } - else -> checkState() - } - } - /** * Will run the Liquibase migration on the actual database. */ - private fun runMigration(existingCheckpoints: Boolean) = doRunMigration(run = true, check = false, existingCheckpoints = existingCheckpoints) + fun runMigration(existingCheckpoints: Boolean) { + migrateOlderDatabaseToUseLiquibase(existingCheckpoints) + doRunMigration(run = true, check = false, existingCheckpoints = existingCheckpoints) + } /** * Ensures that the database is up to date with the latest migration changes. */ - private fun checkState() = doRunMigration(run = false, check = true) + fun checkState() = doRunMigration(run = false, check = true) /** Create a resourse accessor that aggregates the changelogs included in the schemas into one dynamic stream. */ private class CustomResourceAccessor(val dynamicInclude: String, val changelogList: List, classLoader: ClassLoader) : ClassLoaderResourceAccessor(classLoader) { @@ -269,6 +257,6 @@ class CheckpointsException : DatabaseMigrationException("Attempting to update th class DatabaseIncompatibleException(@Suppress("MemberVisibilityCanBePrivate") private val reason: String) : DatabaseMigrationException(errorMessageFor(reason)) { internal companion object { - fun errorMessageFor(reason: String): String = "Incompatible database schema version detected, please run the node with configuration option database.initialiseSchema=true. Reason: $reason" + fun errorMessageFor(reason: String): String = "Incompatible database schema version detected, please run schema migration scripts (node with sub-command run-migration-scripts). Reason: $reason" } } \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/factory/BaseSessionFactoryFactory.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/factory/BaseSessionFactoryFactory.kt index 345b9c6452..e16eafa474 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/factory/BaseSessionFactoryFactory.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/factory/BaseSessionFactoryFactory.kt @@ -5,7 +5,7 @@ import net.corda.core.utilities.contextLogger import net.corda.core.utilities.toHexString import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.nodeapi.internal.persistence.HibernateConfiguration -import net.corda.nodeapi.internal.persistence.SchemaInitializationType +import net.corda.nodeapi.internal.persistence.TransactionIsolationLevel import org.hibernate.SessionFactory import org.hibernate.boot.Metadata import org.hibernate.boot.MetadataBuilder @@ -26,22 +26,19 @@ abstract class BaseSessionFactoryFactory : CordaSessionFactoryFactory { private val logger = contextLogger() } - open fun buildHibernateConfig(databaseConfig: DatabaseConfig, metadataSources: MetadataSources): Configuration { + open fun buildHibernateConfig(databaseConfig: DatabaseConfig, metadataSources: MetadataSources, allowHibernateToManageAppSchema: Boolean): Configuration { val hbm2dll: String = - if (databaseConfig.initialiseSchema && databaseConfig.initialiseAppSchema == SchemaInitializationType.UPDATE) { + if (allowHibernateToManageAppSchema) { "update" - } else if ((!databaseConfig.initialiseSchema && databaseConfig.initialiseAppSchema == SchemaInitializationType.UPDATE) - || databaseConfig.initialiseAppSchema == SchemaInitializationType.VALIDATE) { + } else { "validate" - } else { - "none" } // We set a connection provider as the auto schema generation requires it. The auto schema generation will not // necessarily remain and would likely be replaced by something like Liquibase. For now it is very convenient though. return Configuration(metadataSources).setProperty("hibernate.connection.provider_class", HibernateConfiguration.NodeDatabaseConnectionProvider::class.java.name) .setProperty("hibernate.format_sql", "true") .setProperty("javax.persistence.validation.mode", "none") - .setProperty("hibernate.connection.isolation", databaseConfig.transactionIsolationLevel.jdbcValue.toString()) + .setProperty("hibernate.connection.isolation", TransactionIsolationLevel.default.jdbcValue.toString()) .setProperty("hibernate.hbm2ddl.auto", hbm2dll) .setProperty("hibernate.jdbc.time_zone", "UTC") } @@ -88,12 +85,13 @@ abstract class BaseSessionFactoryFactory : CordaSessionFactoryFactory { databaseConfig: DatabaseConfig, schemas: Set, customClassLoader: ClassLoader?, - attributeConverters: Collection>): SessionFactory { + attributeConverters: Collection>, + allowHibernateToMananageAppSchema: Boolean): SessionFactory { logger.info("Creating session factory for schemas: $schemas") val serviceRegistry = BootstrapServiceRegistryBuilder().build() val metadataSources = MetadataSources(serviceRegistry) - val config = buildHibernateConfig(databaseConfig, metadataSources) + val config = buildHibernateConfig(databaseConfig, metadataSources, allowHibernateToMananageAppSchema) schemas.forEach { schema -> schema.mappedTypes.forEach { config.addAnnotatedClass(it) } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/factory/CordaSessionFactoryFactory.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/factory/CordaSessionFactoryFactory.kt index c55b0bf4db..57b75b763a 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/factory/CordaSessionFactoryFactory.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/factory/CordaSessionFactoryFactory.kt @@ -14,7 +14,8 @@ interface CordaSessionFactoryFactory { databaseConfig: DatabaseConfig, schemas: Set, customClassLoader: ClassLoader?, - attributeConverters: Collection>): SessionFactory + attributeConverters: Collection>, + allowHibernateToMananageAppSchema: Boolean): SessionFactory fun getExtraConfiguration(key: String): Any? fun buildHibernateMetadata(metadataBuilder: MetadataBuilder, attributeConverters: Collection>): Metadata } \ No newline at end of file diff --git a/node/src/integration-test/java/net/corda/serialization/reproduction/GenericReturnFailureReproductionIntegrationTest.java b/node/src/integration-test/java/net/corda/serialization/reproduction/GenericReturnFailureReproductionIntegrationTest.java index b015749f79..ec20e6d5fb 100644 --- a/node/src/integration-test/java/net/corda/serialization/reproduction/GenericReturnFailureReproductionIntegrationTest.java +++ b/node/src/integration-test/java/net/corda/serialization/reproduction/GenericReturnFailureReproductionIntegrationTest.java @@ -1,5 +1,6 @@ package net.corda.serialization.reproduction; +import com.google.common.io.LineProcessor; import net.corda.client.rpc.CordaRPCClient; import net.corda.core.concurrent.CordaFuture; import net.corda.node.services.Permissions; diff --git a/node/src/integration-test/kotlin/net/corda/node/BootTests.kt b/node/src/integration-test/kotlin/net/corda/node/BootTests.kt index 62aae2e54a..d2e741824d 100644 --- a/node/src/integration-test/kotlin/net/corda/node/BootTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/BootTests.kt @@ -44,7 +44,7 @@ class BootTests { rpc.startFlow(::ObjectInputStreamFlow).returnValue.getOrThrow() } } - driver(DriverParameters(cordappsForAllNodes = listOf(enclosedCordapp()))) { + driver(DriverParameters(cordappsForAllNodes = listOf(enclosedCordapp()), allowHibernateToManageAppSchema = false)) { val devModeNode = startNode(devParams).getOrThrow() val node = startNode(ALICE_NAME, devMode = false, parameters = params).getOrThrow() diff --git a/node/src/integration-test/kotlin/net/corda/node/NodeKeystoreCheckTest.kt b/node/src/integration-test/kotlin/net/corda/node/NodeKeystoreCheckTest.kt index dad3fdc467..eca03d81d2 100644 --- a/node/src/integration-test/kotlin/net/corda/node/NodeKeystoreCheckTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/NodeKeystoreCheckTest.kt @@ -17,7 +17,7 @@ import javax.security.auth.x500.X500Principal class NodeKeystoreCheckTest { @Test(timeout=300_000) fun `starting node in non-dev mode with no key store`() { - driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = emptyList())) { + driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = emptyList(), allowHibernateToManageAppSchema = false)) { assertThatThrownBy { startNode(customOverrides = mapOf("devMode" to false)).getOrThrow() }.hasMessageContaining("One or more keyStores (identity or TLS) or trustStore not found.") @@ -26,7 +26,7 @@ class NodeKeystoreCheckTest { @Test(timeout=300_000) fun `node should throw exception if cert path does not chain to the trust root`() { - driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = emptyList())) { + driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = emptyList(), allowHibernateToManageAppSchema = false)) { // Create keystores. val keystorePassword = "password" val certificatesDirectory = baseDirectory(ALICE_NAME) / "certificates" diff --git a/node/src/integration-test/kotlin/net/corda/node/persistence/DbSchemaInitialisationTest.kt b/node/src/integration-test/kotlin/net/corda/node/persistence/DbSchemaInitialisationTest.kt index 3451d2d485..8c8de1a63e 100644 --- a/node/src/integration-test/kotlin/net/corda/node/persistence/DbSchemaInitialisationTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/persistence/DbSchemaInitialisationTest.kt @@ -2,32 +2,21 @@ package net.corda.node.persistence import net.corda.core.utilities.getOrThrow import net.corda.node.flows.isQuasarAgentSpecified -import net.corda.nodeapi.internal.persistence.DatabaseIncompatibleException +import net.corda.node.internal.ConfigurationException import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.NodeParameters import net.corda.testing.driver.driver import org.junit.Test import kotlin.test.assertFailsWith -import kotlin.test.assertNotNull class DbSchemaInitialisationTest { - - @Test(timeout=300_000) - fun `database is initialised`() { + @Test(timeout = 300_000) + fun `database initialisation not allowed in config`() { driver(DriverParameters(startNodesInProcess = isQuasarAgentSpecified(), cordappsForAllNodes = emptyList())) { - val nodeHandle = { - startNode(NodeParameters(customOverrides = mapOf("database.initialiseSchema" to "true"))).getOrThrow() - }() - assertNotNull(nodeHandle) - } - } - - @Test(timeout=300_000) - fun `database is not initialised`() { - driver(DriverParameters(startNodesInProcess = isQuasarAgentSpecified(), cordappsForAllNodes = emptyList())) { - assertFailsWith(DatabaseIncompatibleException::class) { + assertFailsWith(ConfigurationException::class) { startNode(NodeParameters(customOverrides = mapOf("database.initialiseSchema" to "false"))).getOrThrow() } } } + } \ No newline at end of file diff --git a/node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapTest.kt index feacf2c228..34a1b672f2 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapTest.kt @@ -92,7 +92,8 @@ class NetworkMapTest(var initFunc: (URL, NetworkMapServer) -> CompatibilityZoneP internalDriver( portAllocation = portAllocation, compatibilityZone = compatibilityZone, - notarySpecs = emptyList() + notarySpecs = emptyList(), + allowHibernateToManageAppSchema = false ) { val alice = startNode(providedName = ALICE_NAME, devMode = false).getOrThrow() as NodeHandleInternal val nextParams = networkMapServer.networkParameters.copy( @@ -146,7 +147,8 @@ class NetworkMapTest(var initFunc: (URL, NetworkMapServer) -> CompatibilityZoneP internalDriver( portAllocation = portAllocation, compatibilityZone = compatibilityZone, - notarySpecs = emptyList() + notarySpecs = emptyList(), + allowHibernateToManageAppSchema = false ) { val aliceNode = startNode(providedName = ALICE_NAME, devMode = false).getOrThrow() assertDownloadedNetworkParameters(aliceNode) @@ -175,7 +177,8 @@ class NetworkMapTest(var initFunc: (URL, NetworkMapServer) -> CompatibilityZoneP portAllocation = portAllocation, compatibilityZone = compatibilityZone, notarySpecs = emptyList(), - systemProperties = mapOf("net.corda.node.internal.nodeinfo.publish.interval" to 1.seconds.toString()) + systemProperties = mapOf("net.corda.node.internal.nodeinfo.publish.interval" to 1.seconds.toString()), + allowHibernateToManageAppSchema = false ) { val aliceNode = startNode(providedName = ALICE_NAME, devMode = false).getOrThrow() val aliceNodeInfo = aliceNode.nodeInfo.serialize().hash diff --git a/node/src/integration-test/kotlin/net/corda/node/services/rpc/RpcExceptionHandlingTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/rpc/RpcExceptionHandlingTest.kt index 3ae02898e5..92d8c499be 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/rpc/RpcExceptionHandlingTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/rpc/RpcExceptionHandlingTest.kt @@ -57,7 +57,7 @@ class RpcExceptionHandlingTest { } } - driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = listOf(enclosedCordapp()))) { + driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = listOf(enclosedCordapp()), allowHibernateToManageAppSchema = false)) { val devModeNode = startNode(params, BOB_NAME).getOrThrow() val node = startNode(ALICE_NAME, devMode = false, parameters = params).getOrThrow() @@ -76,7 +76,7 @@ class RpcExceptionHandlingTest { rpc.startFlow(::FlowExceptionFlow, expectedMessage, expectedErrorId).returnValue.getOrThrow() } - driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = listOf(enclosedCordapp()))) { + driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = listOf(enclosedCordapp()), allowHibernateToManageAppSchema = false)) { val devModeNode = startNode(params, BOB_NAME).getOrThrow() val node = startNode(ALICE_NAME, devMode = false, parameters = params).getOrThrow() @@ -108,7 +108,7 @@ class RpcExceptionHandlingTest { nodeA.rpc.startFlow(::InitFlow, nodeB.nodeInfo.singleIdentity()).returnValue.getOrThrow() } - driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = listOf(enclosedCordapp()))) { + driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = listOf(enclosedCordapp()), allowHibernateToManageAppSchema = false)) { assertThatThrownBy { scenario(ALICE_NAME, BOB_NAME,true) }.isInstanceOfSatisfying(CordaRuntimeException::class.java) { exception -> diff --git a/node/src/integration-test/kotlin/net/corda/node/services/vault/VaultObserverExceptionTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/vault/VaultObserverExceptionTest.kt index 5cd4529c6d..b22c00f832 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/vault/VaultObserverExceptionTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/vault/VaultObserverExceptionTest.kt @@ -443,11 +443,11 @@ class VaultObserverExceptionTest { val user = User("user", "foo", setOf(Permissions.all())) driver(DriverParameters(startNodesInProcess = true, - cordappsForAllNodes = listOf( - findCordapp("com.r3.dbfailure.contracts"), - findCordapp("com.r3.dbfailure.workflows"), - findCordapp("com.r3.dbfailure.schemas") - ),inMemoryDB = false) + cordappsForAllNodes = listOf( + findCordapp("com.r3.dbfailure.contracts"), + findCordapp("com.r3.dbfailure.workflows"), + findCordapp("com.r3.dbfailure.schemas") + ), inMemoryDB = false) ) { val aliceNode = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow() val bobNode = startNode(providedName = BOB_NAME, rpcUsers = listOf(user)).getOrThrow() @@ -532,12 +532,12 @@ class VaultObserverExceptionTest { val user = User("user", "foo", setOf(Permissions.all())) driver(DriverParameters(startNodesInProcess = true, - cordappsForAllNodes = listOf( - findCordapp("com.r3.dbfailure.contracts"), - findCordapp("com.r3.dbfailure.workflows"), - findCordapp("com.r3.dbfailure.schemas") - ), - inMemoryDB = false) + cordappsForAllNodes = listOf( + findCordapp("com.r3.dbfailure.contracts"), + findCordapp("com.r3.dbfailure.workflows"), + findCordapp("com.r3.dbfailure.schemas") + ), + inMemoryDB = false) ) { val aliceNode = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow() val bobNode = startNode(providedName = BOB_NAME, rpcUsers = listOf(user)).getOrThrow() @@ -609,12 +609,12 @@ class VaultObserverExceptionTest { val user = User("user", "foo", setOf(Permissions.all())) driver(DriverParameters(startNodesInProcess = true, - cordappsForAllNodes = listOf( - findCordapp("com.r3.dbfailure.contracts"), - findCordapp("com.r3.dbfailure.workflows"), - findCordapp("com.r3.dbfailure.schemas") - ), - inMemoryDB = false) + cordappsForAllNodes = listOf( + findCordapp("com.r3.dbfailure.contracts"), + findCordapp("com.r3.dbfailure.workflows"), + findCordapp("com.r3.dbfailure.schemas") + ), + inMemoryDB = false) ) { val aliceNode = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow() val bobNode = startNode(providedName = BOB_NAME, rpcUsers = listOf(user)).getOrThrow() @@ -684,12 +684,12 @@ class VaultObserverExceptionTest { val user = User("user", "foo", setOf(Permissions.all())) driver(DriverParameters(startNodesInProcess = true, - cordappsForAllNodes = listOf( - findCordapp("com.r3.dbfailure.contracts"), - findCordapp("com.r3.dbfailure.workflows"), - findCordapp("com.r3.dbfailure.schemas") - ), - inMemoryDB = false) + cordappsForAllNodes = listOf( + findCordapp("com.r3.dbfailure.contracts"), + findCordapp("com.r3.dbfailure.workflows"), + findCordapp("com.r3.dbfailure.schemas") + ), + inMemoryDB = false) ) { val aliceNode = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow() val bobNode = startNode(providedName = BOB_NAME, rpcUsers = listOf(user)).getOrThrow() @@ -741,12 +741,12 @@ class VaultObserverExceptionTest { fun `Accessing NodeVaultService rawUpdates from a flow is not allowed` () { val user = User("user", "foo", setOf(Permissions.all())) driver(DriverParameters(startNodesInProcess = true, - cordappsForAllNodes = listOf( - findCordapp("com.r3.dbfailure.contracts"), - findCordapp("com.r3.dbfailure.workflows"), - findCordapp("com.r3.dbfailure.schemas") - ), - inMemoryDB = false) + cordappsForAllNodes = listOf( + findCordapp("com.r3.dbfailure.contracts"), + findCordapp("com.r3.dbfailure.workflows"), + findCordapp("com.r3.dbfailure.schemas") + ), + inMemoryDB = false) ) { val aliceNode = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow() @@ -771,12 +771,12 @@ class VaultObserverExceptionTest { val user = User("user", "foo", setOf(Permissions.all())) driver(DriverParameters(startNodesInProcess = true, - cordappsForAllNodes = listOf( - findCordapp("com.r3.dbfailure.contracts"), - findCordapp("com.r3.dbfailure.workflows"), - findCordapp("com.r3.transactionfailure.workflows"), - findCordapp("com.r3.dbfailure.schemas")), - inMemoryDB = false) + cordappsForAllNodes = listOf( + findCordapp("com.r3.dbfailure.contracts"), + findCordapp("com.r3.dbfailure.workflows"), + findCordapp("com.r3.transactionfailure.workflows"), + findCordapp("com.r3.dbfailure.schemas")), + inMemoryDB = false) ) { val aliceNode = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow() @@ -802,12 +802,12 @@ class VaultObserverExceptionTest { val user = User("user", "foo", setOf(Permissions.all())) driver(DriverParameters(startNodesInProcess = true, - cordappsForAllNodes = listOf( - findCordapp("com.r3.dbfailure.contracts"), - findCordapp("com.r3.dbfailure.workflows"), - findCordapp("com.r3.transactionfailure.workflows"), - findCordapp("com.r3.dbfailure.schemas")), - inMemoryDB = false) + cordappsForAllNodes = listOf( + findCordapp("com.r3.dbfailure.contracts"), + findCordapp("com.r3.dbfailure.workflows"), + findCordapp("com.r3.transactionfailure.workflows"), + findCordapp("com.r3.dbfailure.schemas")), + inMemoryDB = false) ) { // Subscribing with custom SafeSubscriber; the custom SafeSubscriber will not get replaced by a ResilientSubscriber // meaning that it will behave as a SafeSubscriber; it will get unsubscribed upon throwing an error. diff --git a/node/src/main/kotlin/net/corda/node/NodeCmdLineOptions.kt b/node/src/main/kotlin/net/corda/node/NodeCmdLineOptions.kt index d530ae4d41..ed2e034550 100644 --- a/node/src/main/kotlin/net/corda/node/NodeCmdLineOptions.kt +++ b/node/src/main/kotlin/net/corda/node/NodeCmdLineOptions.kt @@ -48,6 +48,14 @@ open class SharedNodeCmdLineOptions { ) var devMode: Boolean? = null + @Option( + names = ["--allow-hibernate-to-manage-app-schema"], + description = ["Allows hibernate to create/modify app schema for CorDapps based on their mapped schema.", + "Use this for rapid app development or for compatibility with pre-4.6 CorDapps.", + "Only available in dev mode."] + ) + var allowHibernateToManangeAppSchema: Boolean = false + open fun parseConfiguration(configuration: Config): Valid { val option = Configuration.Options(strict = unknownConfigKeysPolicy == UnknownConfigKeysPolicy.FAIL) return configuration.parseAsNodeConfiguration(option) diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 9c015067e3..996de2a760 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -210,7 +210,8 @@ abstract class AbstractNode(val configuration: NodeConfiguration, val serverThread: AffinityExecutor.ServiceAffinityExecutor, val busyNodeLatch: ReusableLatch = ReusableLatch(), djvmBootstrapSource: ApiSource = EmptyApi, - djvmCordaSource: UserSource? = null) : SingletonSerializeAsToken() { + djvmCordaSource: UserSource? = null, + protected val allowHibernateToManageAppSchema: Boolean = false) : SingletonSerializeAsToken() { protected abstract val log: Logger @Suppress("LeakingThis") @@ -222,6 +223,11 @@ abstract class AbstractNode(val configuration: NodeConfiguration, protected val runOnStop = ArrayList<() -> Any?>() + protected open val runMigrationScripts: Boolean = configuredDbIsInMemory() + + // if the configured DB is in memory, we will need to run db migrations, as the db does not persist between runs. + private fun configuredDbIsInMemory() = configuration.dataSourceProperties.getProperty("dataSource.url").startsWith("jdbc:h2:mem:") + init { (serverThread as? ExecutorService)?.let { runOnStop += { @@ -233,6 +239,12 @@ abstract class AbstractNode(val configuration: NodeConfiguration, } quasarExcludePackages(configuration) + + if (allowHibernateToManageAppSchema && !configuration.devMode) { + throw ConfigurationException("Hibernate can only be used to manage app schema in development while using dev mode. " + + "Please remove the --allow-hibernate-to-manage-app-schema command line flag and provide schema migration scripts for your CorDapps." + ) + } } private val notaryLoader = configuration.notary?.let { @@ -248,7 +260,8 @@ abstract class AbstractNode(val configuration: NodeConfiguration, schemaService, configuration.dataSourceProperties, cacheFactory, - cordappLoader.appClassLoader) + cordappLoader.appClassLoader, + allowHibernateToManageAppSchema) private val transactionSupport = CordaTransactionSupportImpl(database) @@ -458,6 +471,33 @@ abstract class AbstractNode(val configuration: NodeConfiguration, } } + open fun runDatabaseMigrationScripts() { + check(started == null) { "Node has already been started" } + Node.printBasicNodeInfo("Running database schema migration scripts ...") + val props = configuration.dataSourceProperties + if (props.isEmpty) throw DatabaseConfigurationException("There must be a database configured.") + database.startHikariPool(props, schemaService.internalSchemas(), metricRegistry, this.cordappLoader, configuration.baseDirectory, configuration.myLegalName, runMigrationScripts = true) + // Now log the vendor string as this will also cause a connection to be tested eagerly. + logVendorString(database, log) + if (allowHibernateToManageAppSchema) { + Node.printBasicNodeInfo("Initialising CorDapps to get schemas created by hibernate") + val trustRoot = initKeyStores() + networkMapClient?.start(trustRoot) + val (netParams, signedNetParams) = NetworkParametersReader(trustRoot, networkMapClient, configuration.baseDirectory).read() + log.info("Loaded network parameters: $netParams") + check(netParams.minimumPlatformVersion <= versionInfo.platformVersion) { + "Node's platform version is lower than network's required minimumPlatformVersion" + } + networkMapCache.start(netParams.notaries) + + database.transaction { + networkParametersStorage.setCurrentParameters(signedNetParams, trustRoot) + cordappProvider.start() + } + } + Node.printBasicNodeInfo("Database migration done.") + } + open fun start(): S { check(started == null) { "Node has already been started" } @@ -946,7 +986,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, protected open fun startDatabase() { val props = configuration.dataSourceProperties if (props.isEmpty) throw DatabaseConfigurationException("There must be a database configured.") - database.startHikariPool(props, configuration.database, schemaService.internalSchemas(), metricRegistry, this.cordappLoader, configuration.baseDirectory, configuration.myLegalName) + database.startHikariPool(props, schemaService.internalSchemas(), metricRegistry, this.cordappLoader, configuration.baseDirectory, configuration.myLegalName, runMigrationScripts = runMigrationScripts) // Now log the vendor string as this will also cause a connection to be tested eagerly. logVendorString(database, log) } @@ -1313,13 +1353,15 @@ class FlowStarterImpl(private val smm: StateMachineManager, private val flowLogi class ConfigurationException(message: String) : CordaException(message) +@Suppress("LongParameterList") fun createCordaPersistence(databaseConfig: DatabaseConfig, wellKnownPartyFromX500Name: (CordaX500Name) -> Party?, wellKnownPartyFromAnonymous: (AbstractParty) -> Party?, schemaService: SchemaService, hikariProperties: Properties, cacheFactory: NamedCacheFactory, - customClassLoader: ClassLoader?): CordaPersistence { + customClassLoader: ClassLoader?, + allowHibernateToManageAppSchema: Boolean = false): CordaPersistence { // Register the AbstractPartyDescriptor so Hibernate doesn't warn when encountering AbstractParty. Unfortunately // Hibernate warns about not being able to find a descriptor if we don't provide one, but won't use it by default // so we end up providing both descriptor and converter. We should re-examine this in later versions to see if @@ -1330,25 +1372,38 @@ fun createCordaPersistence(databaseConfig: DatabaseConfig, val jdbcUrl = hikariProperties.getProperty("dataSource.url", "") return CordaPersistence( - databaseConfig, - schemaService.schemas, - jdbcUrl, - cacheFactory, - attributeConverters, customClassLoader, - errorHandler = { e -> - // "corrupting" a DatabaseTransaction only inside a flow state machine execution - FlowStateMachineImpl.currentStateMachine()?.let { - // register only the very first exception thrown throughout a chain of logical transactions - setException(e) - } - }) + databaseConfig, + schemaService.schemas, + jdbcUrl, + cacheFactory, + attributeConverters, customClassLoader, + errorHandler = { e -> + // "corrupting" a DatabaseTransaction only inside a flow state machine execution + FlowStateMachineImpl.currentStateMachine()?.let { + // register only the very first exception thrown throughout a chain of logical transactions + setException(e) + } + }, + allowHibernateToManageAppSchema = allowHibernateToManageAppSchema) } -fun CordaPersistence.startHikariPool(hikariProperties: Properties, databaseConfig: DatabaseConfig, schemas: Set, metricRegistry: MetricRegistry? = null, cordappLoader: CordappLoader? = null, currentDir: Path? = null, ourName: CordaX500Name) { +@Suppress("LongParameterList", "ComplexMethod", "ThrowsCount") +fun CordaPersistence.startHikariPool( + hikariProperties: Properties, + schemas: Set, + metricRegistry: MetricRegistry? = null, + cordappLoader: CordappLoader? = null, + currentDir: Path? = null, + ourName: CordaX500Name, + runMigrationScripts: Boolean = false) { try { val dataSource = DataSourceFactory.createDataSource(hikariProperties, metricRegistry = metricRegistry) - val schemaMigration = SchemaMigration(schemas, dataSource, databaseConfig, cordappLoader, currentDir, ourName) - schemaMigration.nodeStartup(dataSource.connection.use { DBCheckpointStorage().getCheckpointCount(it) != 0L }) + val schemaMigration = SchemaMigration(schemas, dataSource, cordappLoader, currentDir, ourName) + if (runMigrationScripts) { + schemaMigration.runMigration(dataSource.connection.use { DBCheckpointStorage().getCheckpointCount(it) != 0L }) + } else { + schemaMigration.checkState() + } start(dataSource) } catch (ex: Exception) { when { diff --git a/node/src/main/kotlin/net/corda/node/internal/Node.kt b/node/src/main/kotlin/net/corda/node/internal/Node.kt index 77d95abacd..884d2b57b1 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -125,7 +125,8 @@ open class Node(configuration: NodeConfiguration, flowManager: FlowManager = NodeFlowManager(configuration.flowOverrides), cacheFactoryPrototype: BindableNamedCacheFactory = DefaultNamedCacheFactory(), djvmBootstrapSource: ApiSource = createBootstrapSource(configuration), - djvmCordaSource: UserSource? = createCordaSource(configuration) + djvmCordaSource: UserSource? = createCordaSource(configuration), + allowHibernateToManageAppSchema: Boolean = false ) : AbstractNode( configuration, createClock(configuration), @@ -135,7 +136,8 @@ open class Node(configuration: NodeConfiguration, // Under normal (non-test execution) it will always be "1" AffinityExecutor.ServiceAffinityExecutor("Node thread-${sameVmNodeCounter.incrementAndGet()}", 1), djvmBootstrapSource = djvmBootstrapSource, - djvmCordaSource = djvmCordaSource + djvmCordaSource = djvmCordaSource, + allowHibernateToManageAppSchema = allowHibernateToManageAppSchema ) { override fun createStartedNode(nodeInfo: NodeInfo, rpcOps: CordaRPCOps, notaryService: NotaryService?): NodeInfo = @@ -559,6 +561,13 @@ open class Node(configuration: NodeConfiguration, return super.generateAndSaveNodeInfo() } + override fun runDatabaseMigrationScripts() { + if (allowHibernateToManageAppSchema) { + initialiseSerialization() + } + super.runDatabaseMigrationScripts() + } + override fun start(): NodeInfo { registerDefaultExceptionHandler() initialiseSerialization() 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 d9167e2816..8099451e3e 100644 --- a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt +++ b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt @@ -76,10 +76,16 @@ open class NodeStartupCli : CordaCliWrapper("corda", "Runs a Corda Node") { private val justGenerateRpcSslCertsCli by lazy { GenerateRpcSslCertsCli(startup) } private val initialRegistrationCli by lazy { InitialRegistrationCli(startup) } private val validateConfigurationCli by lazy { ValidateConfigurationCli() } + private val runMigrationScriptsCli by lazy { RunMigrationScriptsCli(startup) } override fun initLogging(): Boolean = this.initLogging(cmdLineOptions.baseDirectory) - override fun additionalSubCommands() = setOf(networkCacheCli, justGenerateNodeInfoCli, justGenerateRpcSslCertsCli, initialRegistrationCli, validateConfigurationCli) + override fun additionalSubCommands() = setOf(networkCacheCli, + justGenerateNodeInfoCli, + justGenerateRpcSslCertsCli, + initialRegistrationCli, + validateConfigurationCli, + runMigrationScriptsCli) override fun call(): Int { if (!validateBaseDirectory()) { @@ -201,7 +207,7 @@ open class NodeStartup : NodeStartupLogging { protected open fun preNetworkRegistration(conf: NodeConfiguration) = Unit - open fun createNode(conf: NodeConfiguration, versionInfo: VersionInfo): Node = Node(conf, versionInfo) + open fun createNode(conf: NodeConfiguration, versionInfo: VersionInfo): Node = Node(conf, versionInfo, allowHibernateToManageAppSchema = cmdLineOptions.allowHibernateToManangeAppSchema) fun startNode(node: Node, startTime: Long) { if (node.configuration.devMode) { diff --git a/node/src/main/kotlin/net/corda/node/internal/subcommands/RunMigrationScriptsCli.kt b/node/src/main/kotlin/net/corda/node/internal/subcommands/RunMigrationScriptsCli.kt new file mode 100644 index 0000000000..ff707e1ae5 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/subcommands/RunMigrationScriptsCli.kt @@ -0,0 +1,16 @@ +package net.corda.node.internal.subcommands + +import net.corda.node.internal.Node +import net.corda.node.internal.NodeCliCommand +import net.corda.node.internal.NodeStartup +import net.corda.node.internal.RunAfterNodeInitialisation + +class RunMigrationScriptsCli(startup: NodeStartup) : NodeCliCommand("run-migration-scripts", "Run the database migration scripts and create or update schemas", startup) { + override fun runProgram(): Int { + return startup.initialiseAndRun(cmdLineOptions, object : RunAfterNodeInitialisation { + override fun run(node: Node) { + node.runDatabaseMigrationScripts() + } + }) + } +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/config/NodeConfigurationImpl.kt b/node/src/main/kotlin/net/corda/node/services/config/NodeConfigurationImpl.kt index e1dcc86903..782c1a40dc 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/NodeConfigurationImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/NodeConfigurationImpl.kt @@ -15,7 +15,6 @@ import net.corda.nodeapi.internal.config.MutualSslConfiguration import net.corda.nodeapi.internal.config.SslConfiguration import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.persistence.DatabaseConfig -import net.corda.nodeapi.internal.persistence.SchemaInitializationType import net.corda.tools.shell.SSHDConfiguration import java.net.URL import java.nio.file.Path @@ -129,8 +128,6 @@ data class NodeConfigurationImpl( fun messagingServerExternal(messagingServerAddress: NetworkHostAndPort?) = messagingServerAddress != null fun database(devMode: Boolean) = DatabaseConfig( - initialiseSchema = devMode, - initialiseAppSchema = if(devMode) SchemaInitializationType.UPDATE else SchemaInitializationType.VALIDATE, exportHibernateJMXStatistics = devMode ) } diff --git a/node/src/main/kotlin/net/corda/node/services/config/schema/v1/ConfigSections.kt b/node/src/main/kotlin/net/corda/node/services/config/schema/v1/ConfigSections.kt index 0132ecee53..19b289df98 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/schema/v1/ConfigSections.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/schema/v1/ConfigSections.kt @@ -14,6 +14,7 @@ import net.corda.common.validation.internal.Validated.Companion.invalid import net.corda.common.validation.internal.Validated.Companion.valid import net.corda.core.context.AuthServiceId import net.corda.core.internal.notary.NotaryServiceFlow +import net.corda.node.internal.ConfigurationException import net.corda.node.services.config.AuthDataSourceType import net.corda.node.services.config.CertChainPolicyConfig import net.corda.node.services.config.CertChainPolicyType @@ -44,7 +45,6 @@ import net.corda.nodeapi.BrokerRpcSslOptions import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.nodeapi.internal.persistence.TransactionIsolationLevel -import net.corda.nodeapi.internal.persistence.SchemaInitializationType import net.corda.notary.experimental.bftsmart.BFTSmartConfig import net.corda.notary.experimental.raft.RaftConfig import net.corda.tools.shell.SSHDConfiguration @@ -267,16 +267,32 @@ internal object SSHDConfigurationSpec : Configuration.Specification = attempt { SSHDConfiguration(configuration.withOptions(options)[port]) } } +enum class SchemaInitializationType{ + NONE, + VALIDATE, + UPDATE +} + internal object DatabaseConfigSpec : Configuration.Specification("DatabaseConfig") { - private val initialiseSchema by boolean().optional().withDefaultValue(DatabaseConfig.Defaults.initialiseSchema) - private val initialiseAppSchema by enum(SchemaInitializationType::class).optional().withDefaultValue(DatabaseConfig.Defaults.initialiseAppSchema) - private val transactionIsolationLevel by enum(TransactionIsolationLevel::class).optional().withDefaultValue(DatabaseConfig.Defaults.transactionIsolationLevel) + private val initialiseSchema by boolean().optional() + private val initialiseAppSchema by enum(SchemaInitializationType::class).optional() + private val transactionIsolationLevel by enum(TransactionIsolationLevel::class).optional() private val exportHibernateJMXStatistics by boolean().optional().withDefaultValue(DatabaseConfig.Defaults.exportHibernateJMXStatistics) private val mappedSchemaCacheSize by long().optional().withDefaultValue(DatabaseConfig.Defaults.mappedSchemaCacheSize) override fun parseValid(configuration: Config, options: Configuration.Options): Valid { + if (initialiseSchema.isSpecifiedBy(configuration)){ + throw ConfigurationException("Unsupported configuration database/initialiseSchema - this option has been removed, please use the run-migration-scripts sub-command or the database management tool to modify schemas") + } + if (initialiseAppSchema.isSpecifiedBy(configuration)){ + throw ConfigurationException("Unsupported configuration database/initialiseAppSchema - this option has been removed, please use the run-migration-scripts sub-command or the database management tool to modify schemas") + } + if (transactionIsolationLevel.isSpecifiedBy(configuration)){ + throw ConfigurationException("Unsupported configuration database/transactionIsolationLevel - this option has been removed and cannot be changed") + } val config = configuration.withOptions(options) - return valid(DatabaseConfig(config[initialiseSchema], config[initialiseAppSchema], config[transactionIsolationLevel], config[exportHibernateJMXStatistics], config[mappedSchemaCacheSize])) + + return valid(DatabaseConfig(config[exportHibernateJMXStatistics], config[mappedSchemaCacheSize])) } } diff --git a/node/src/main/resources/reference.conf b/node/src/main/resources/reference.conf index 02539aef51..9dc12306e6 100644 --- a/node/src/main/resources/reference.conf +++ b/node/src/main/resources/reference.conf @@ -1,7 +1,6 @@ additionalP2PAddresses = [] crlCheckSoftFail = true database = { - transactionIsolationLevel = "REPEATABLE_READ" exportHibernateJMXStatistics = "false" } dataSourceProperties = { diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/DbMapDeadlockTest.kt b/node/src/test/kotlin/net/corda/node/services/persistence/DbMapDeadlockTest.kt index 45c800aaf8..5befebeb6e 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/DbMapDeadlockTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/DbMapDeadlockTest.kt @@ -7,7 +7,6 @@ import net.corda.node.internal.startHikariPool import net.corda.node.services.schema.NodeSchemaService import net.corda.node.utilities.AppendOnlyPersistentMap import net.corda.nodeapi.internal.persistence.DatabaseConfig -import net.corda.nodeapi.internal.persistence.TransactionIsolationLevel import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.TestIdentity import net.corda.testing.internal.TestingNamedCacheFactory @@ -91,10 +90,10 @@ class DbMapDeadlockTest { fun recreateDeadlock(hikariProperties: Properties) { val cacheFactory = TestingNamedCacheFactory() - val dbConfig = DatabaseConfig(initialiseSchema = true, transactionIsolationLevel = TransactionIsolationLevel.READ_COMMITTED) + val dbConfig = DatabaseConfig() val schemaService = NodeSchemaService(extraSchemas = setOf(LockDbSchemaV2)) createCordaPersistence(dbConfig, { null }, { null }, schemaService, hikariProperties, cacheFactory, null).apply { - startHikariPool(hikariProperties, dbConfig, schemaService.schemas, ourName = TestIdentity(ALICE_NAME, 70).name) + startHikariPool(hikariProperties, schemaService.schemas, ourName = TestIdentity(ALICE_NAME, 70).name, runMigrationScripts = true) }.use { persistence -> // First clean up any remains from previous test runs diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt b/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt index a4f0a47e8c..dfdc5530ad 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt @@ -48,6 +48,7 @@ import net.corda.testing.internal.vault.VaultFiller import net.corda.testing.node.MockServices import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties import org.assertj.core.api.Assertions +import org.assertj.core.api.Assertions.`in` import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy import org.hibernate.SessionFactory @@ -976,7 +977,7 @@ class HibernateConfigurationTest { doReturn(it.party).whenever(mock).wellKnownPartyFromX500Name(it.name) } } - database = configureDatabase(dataSourceProps, DatabaseConfig(initialiseSchema = initialiseSchema), identityService::wellKnownPartyFromX500Name, identityService::wellKnownPartyFromAnonymous, schemaService) + database = configureDatabase(dataSourceProps, DatabaseConfig(), identityService::wellKnownPartyFromX500Name, identityService::wellKnownPartyFromAnonymous, schemaService, runMigrationScripts = initialiseSchema, allowHibernateToManageAppSchema = initialiseSchema) return database } diff --git a/node/src/test/resources/test-config-quasarexcludepackages.conf b/node/src/test/resources/test-config-quasarexcludepackages.conf index 512a2a228c..5d85a6c9a2 100644 --- a/node/src/test/resources/test-config-quasarexcludepackages.conf +++ b/node/src/test/resources/test-config-quasarexcludepackages.conf @@ -12,7 +12,6 @@ dataSourceProperties = { dataSource.password = "" } database = { - transactionIsolationLevel = "REPEATABLE_READ" exportHibernateJMXStatistics = "false" } p2pAddress = "localhost:2233" diff --git a/node/src/test/resources/working-config.conf b/node/src/test/resources/working-config.conf index ed1d05deb6..773703d211 100644 --- a/node/src/test/resources/working-config.conf +++ b/node/src/test/resources/working-config.conf @@ -12,7 +12,6 @@ dataSourceProperties = { dataSource.password = "" } database = { - transactionIsolationLevel = "REPEATABLE_READ" exportHibernateJMXStatistics = "false" } p2pAddress = "localhost:2233" diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt index 8c16c40a34..2597aa87b1 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt @@ -202,7 +202,8 @@ fun driver(defaultParameters: DriverParameters = DriverParameters(), dsl: Dr cordappsForAllNodes = uncheckedCast(defaultParameters.cordappsForAllNodes), djvmBootstrapSource = defaultParameters.djvmBootstrapSource, djvmCordaSource = defaultParameters.djvmCordaSource, - environmentVariables = defaultParameters.environmentVariables + environmentVariables = defaultParameters.environmentVariables, + allowHibernateToManageAppSchema = defaultParameters.allowHibernateToManageAppSchema ), coerce = { it }, dsl = dsl @@ -263,7 +264,8 @@ data class DriverParameters( val cordappsForAllNodes: Collection? = null, val djvmBootstrapSource: Path? = null, val djvmCordaSource: List = emptyList(), - val environmentVariables : Map = emptyMap() + val environmentVariables : Map = emptyMap(), + val allowHibernateToManageAppSchema: Boolean = true ) { constructor(cordappsForAllNodes: Collection) : this(isDebug = false, cordappsForAllNodes = cordappsForAllNodes) @@ -424,6 +426,7 @@ data class DriverParameters( fun withDjvmBootstrapSource(djvmBootstrapSource: Path?): DriverParameters = copy(djvmBootstrapSource = djvmBootstrapSource) fun withDjvmCordaSource(djvmCordaSource: List): DriverParameters = copy(djvmCordaSource = djvmCordaSource) fun withEnvironmentVariables(variables : Map): DriverParameters = copy(environmentVariables = variables) + fun withAllowHibernateToManageAppSchema(value: Boolean): DriverParameters = copy(allowHibernateToManageAppSchema = value) fun copy( isDebug: Boolean, diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt index 56e5b703da..f0357e535f 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt @@ -1,4 +1,4 @@ -@file:Suppress("TooManyFunctions") +@file:Suppress("TooManyFunctions", "Deprecation") package net.corda.testing.node.internal import co.paralleluniverse.fibers.instrument.JavaAgent @@ -15,6 +15,7 @@ import net.corda.core.concurrent.firstOf import net.corda.core.identity.CordaX500Name import net.corda.core.internal.PLATFORM_VERSION import net.corda.core.internal.ThreadBox +import net.corda.core.internal.concurrent.doOnComplete import net.corda.core.internal.concurrent.doOnError import net.corda.core.internal.concurrent.doneFuture import net.corda.core.internal.concurrent.flatMap @@ -22,12 +23,12 @@ import net.corda.core.internal.concurrent.fork import net.corda.core.internal.concurrent.map import net.corda.core.internal.concurrent.openFuture import net.corda.core.internal.concurrent.transpose -import net.corda.core.internal.cordapp.CordappImpl.Companion.CORDAPP_CONTRACT_NAME import net.corda.core.internal.cordapp.CordappImpl.Companion.CORDAPP_CONTRACT_LICENCE +import net.corda.core.internal.cordapp.CordappImpl.Companion.CORDAPP_CONTRACT_NAME import net.corda.core.internal.cordapp.CordappImpl.Companion.CORDAPP_CONTRACT_VENDOR import net.corda.core.internal.cordapp.CordappImpl.Companion.CORDAPP_CONTRACT_VERSION -import net.corda.core.internal.cordapp.CordappImpl.Companion.CORDAPP_WORKFLOW_NAME import net.corda.core.internal.cordapp.CordappImpl.Companion.CORDAPP_WORKFLOW_LICENCE +import net.corda.core.internal.cordapp.CordappImpl.Companion.CORDAPP_WORKFLOW_NAME import net.corda.core.internal.cordapp.CordappImpl.Companion.CORDAPP_WORKFLOW_VENDOR import net.corda.core.internal.cordapp.CordappImpl.Companion.CORDAPP_WORKFLOW_VERSION import net.corda.core.internal.cordapp.CordappImpl.Companion.MIN_PLATFORM_VERSION @@ -51,6 +52,7 @@ import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.contextLogger import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.millis +import net.corda.coretesting.internal.stubs.CertificateStoreStubs import net.corda.node.NodeRegistrationOption import net.corda.node.VersionInfo import net.corda.node.internal.Node @@ -82,11 +84,17 @@ import net.corda.notary.experimental.raft.RaftConfig import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME import net.corda.testing.core.DUMMY_BANK_A_NAME -import net.corda.testing.driver.* +import net.corda.testing.driver.DriverDSL +import net.corda.testing.driver.DriverParameters +import net.corda.testing.driver.JmxPolicy +import net.corda.testing.driver.NodeHandle +import net.corda.testing.driver.NodeParameters +import net.corda.testing.driver.NotaryHandle +import net.corda.testing.driver.PortAllocation +import net.corda.testing.driver.WebserverHandle import net.corda.testing.driver.internal.InProcessImpl import net.corda.testing.driver.internal.NodeHandleInternal import net.corda.testing.driver.internal.OutOfProcessImpl -import net.corda.coretesting.internal.stubs.CertificateStoreStubs import net.corda.testing.node.ClusterSpec import net.corda.testing.node.NotarySpec import okhttp3.OkHttpClient @@ -139,7 +147,8 @@ class DriverDSLImpl( val cordappsForAllNodes: Collection?, val djvmBootstrapSource: Path?, val djvmCordaSource: List, - val environmentVariables : Map + val environmentVariables : Map, + val allowHibernateToManageAppSchema: Boolean = true ) : InternalDriverDSL { private var _executorService: ScheduledExecutorService? = null @@ -248,30 +257,40 @@ class DriverDSLImpl( // TODO: Derive name from the full picked name, don't just wrap the common name val name = parameters.providedName ?: CordaX500Name("${oneOf(names).organisation}-${p2pAddress.port}", "London", "GB") + val config = createConfig(name, parameters, p2pAddress) val registrationFuture = if (compatibilityZone?.rootCert != null) { // We don't need the network map to be available to be able to register the node - startNodeRegistration(name, compatibilityZone.rootCert, compatibilityZone.config(), parameters.customOverrides) + createSchema(config, false).doOnComplete { startNodeRegistration(it, compatibilityZone.rootCert, compatibilityZone.config()) } } else { - doneFuture(Unit) + doneFuture(config) } - return registrationFuture.flatMap { - networkMapAvailability.flatMap { + return registrationFuture.flatMap { conf -> + networkMapAvailability.flatMap {networkMap -> // But starting the node proper does require the network map - startRegisteredNode(name, it, parameters, p2pAddress, bytemanPort) + startRegisteredNode(conf, networkMap, parameters, bytemanPort) } } } @Suppress("ComplexMethod") - private fun startRegisteredNode(name: CordaX500Name, + private fun startRegisteredNode(config: NodeConfig, localNetworkMap: LocalNetworkMap?, parameters: NodeParameters, - p2pAddress: NetworkHostAndPort = portAllocation.nextHostAndPort(), bytemanPort: Int? = null): CordaFuture { + val webAddress = portAllocation.nextHostAndPort() + return startNodeInternal(config, webAddress, localNetworkMap, parameters, bytemanPort) + } + + @Suppress("ComplexMethod") + private fun createConfig( + providedName: CordaX500Name, + parameters: NodeParameters, + p2pAddress: NetworkHostAndPort = portAllocation.nextHostAndPort() + ): NodeConfig { + val baseDirectory = baseDirectory(providedName).createDirectories() val rpcAddress = portAllocation.nextHostAndPort() val rpcAdminAddress = portAllocation.nextHostAndPort() - val webAddress = portAllocation.nextHostAndPort() val users = parameters.rpcUsers.map { it.copy(permissions = it.permissions + DRIVER_REQUIRED_PERMISSIONS) } val czUrlConfig = when (compatibilityZone) { null -> emptyMap() @@ -292,50 +311,41 @@ class DriverDSLImpl( val flowOverrideConfig = FlowOverrideConfig(parameters.flowOverrides.map { FlowOverride(it.key.canonicalName, it.value.canonicalName) }) val overrides = configOf( - NodeConfiguration::myLegalName.name to name.toString(), + NodeConfiguration::myLegalName.name to providedName.toString(), NodeConfiguration::p2pAddress.name to p2pAddress.toString(), "rpcSettings.address" to rpcAddress.toString(), "rpcSettings.adminAddress" to rpcAdminAddress.toString(), NodeConfiguration::useTestClock.name to useTestClock, - NodeConfiguration::rpcUsers.name to if (users.isEmpty()) defaultRpcUserList else users.map { it.toConfig().root().unwrapped() }, + NodeConfiguration::rpcUsers.name to if (users.isEmpty()) defaultRpcUserList else users.map { + it.toConfig().root().unwrapped() + }, NodeConfiguration::verifierType.name to parameters.verifierType.name, NodeConfiguration::flowOverrides.name to flowOverrideConfig.toConfig().root().unwrapped(), NodeConfiguration::additionalNodeInfoPollingFrequencyMsec.name to 1000 ) + czUrlConfig + jmxConfig + parameters.customOverrides - val config = NodeConfig( + return NodeConfig( ConfigHelper.loadConfig( - baseDirectory = baseDirectory(name), + baseDirectory = baseDirectory, allowMissingConfig = true, configOverrides = if (overrides.hasPath("devMode")) overrides else overrides + mapOf("devMode" to true) ).withDJVMConfig(djvmBootstrapSource, djvmCordaSource) ).checkAndOverrideForInMemoryDB() - return startNodeInternal(config, webAddress, localNetworkMap, parameters, bytemanPort) + } + + private fun createSchema(config: NodeConfig, hibernateForAppSchema: Boolean): CordaFuture { + if (startNodesInProcess || inMemoryDB) return doneFuture(config) + return startOutOfProcessMiniNode(config, + listOfNotNull( + "run-migration-scripts", + if (hibernateForAppSchema) "--allow-hibernate-to-manage-app-schema" else null + ).toTypedArray()).map { config } } private fun startNodeRegistration( - providedName: CordaX500Name, + config: NodeConfig, rootCert: X509Certificate, - networkServicesConfig: NetworkServicesConfig, - customOverrides: Map = mapOf() + networkServicesConfig: NetworkServicesConfig ): CordaFuture { - val baseDirectory = baseDirectory(providedName).createDirectories() - val overrides = configOf( - "p2pAddress" to portAllocation.nextHostAndPort().toString(), - "compatibilityZoneURL" to networkServicesConfig.doormanURL.toString(), - "myLegalName" to providedName.toString(), - "rpcSettings" to mapOf( - "address" to portAllocation.nextHostAndPort().toString(), - "adminAddress" to portAllocation.nextHostAndPort().toString() - ), - "additionalNodeInfoPollingFrequencyMsec" to 1000, - "devMode" to false) + customOverrides - val config = NodeConfig( - ConfigHelper.loadConfig( - baseDirectory = baseDirectory, - allowMissingConfig = true, - configOverrides = overrides - ).withDJVMConfig(djvmBootstrapSource, djvmCordaSource) - ).checkAndOverrideForInMemoryDB() val versionInfo = VersionInfo(PLATFORM_VERSION, "1", "1", "1") config.corda.certificatesDirectory.createDirectories() @@ -410,7 +420,7 @@ class DriverDSLImpl( val notaryInfosFuture = if (compatibilityZone == null) { // If no CZ is specified then the driver does the generation of the network parameters and the copying of the // node info files. - startNotaryIdentityGeneration().map { notaryInfos -> Pair(notaryInfos, LocalNetworkMap(notaryInfos)) } + startNotaryIdentityGeneration().map { notaryInfos -> Pair(notaryInfos, LocalNetworkMap(notaryInfos.map{it.second})) } } else { // Otherwise it's the CZ's job to distribute thse via the HTTP network map, as that is what the nodes will be expecting. val notaryInfosFuture = if (compatibilityZone.rootCert == null) { @@ -421,7 +431,7 @@ class DriverDSLImpl( startAllNotaryRegistrations(compatibilityZone.rootCert, compatibilityZone) } notaryInfosFuture.map { notaryInfos -> - compatibilityZone.publishNotaries(notaryInfos) + compatibilityZone.publishNotaries(notaryInfos.map{it.second}) Pair(notaryInfos, null) } } @@ -429,9 +439,9 @@ class DriverDSLImpl( networkMapAvailability = notaryInfosFuture.map { it.second } _notaries = notaryInfosFuture.map { (notaryInfos, localNetworkMap) -> - val listOfFutureNodeHandles = startNotaries(localNetworkMap, notaryCustomOverrides) - notaryInfos.zip(listOfFutureNodeHandles) { (identity, validating), nodeHandlesFuture -> - NotaryHandle(identity, validating, nodeHandlesFuture) + val listOfFutureNodeHandles = startNotaries(notaryInfos.map{it.first}, localNetworkMap, notaryCustomOverrides) + notaryInfos.zip(listOfFutureNodeHandles) { (_, notaryInfo), nodeHandlesFuture -> + NotaryHandle(notaryInfo.identity, notaryInfo.validating, nodeHandlesFuture) } } try { @@ -471,9 +481,12 @@ class DriverDSLImpl( } } - private fun startNotaryIdentityGeneration(): CordaFuture> { + private fun startNotaryIdentityGeneration(): CordaFuture>> { return executorService.fork { notarySpecs.map { spec -> + val notaryConfig = mapOf("notary" to mapOf("validating" to spec.validating)) + val parameters = NodeParameters(rpcUsers = spec.rpcUsers, verifierType = spec.verifierType, customOverrides = notaryConfig + notaryCustomOverrides, maximumHeapSize = spec.maximumHeapSize) + val config = createConfig(spec.name, parameters) val identity = when (spec.cluster) { null -> { DevIdentityGenerator.installKeyStoreWithNodeIdentity(baseDirectory(spec.name), spec.name) @@ -499,14 +512,14 @@ class DriverDSLImpl( } else -> throw UnsupportedOperationException("Cluster spec ${spec.cluster} not supported by Driver") } - NotaryInfo(identity, spec.validating) + Pair(config, NotaryInfo(identity, spec.validating)) } } } private fun startAllNotaryRegistrations( rootCert: X509Certificate, - compatibilityZone: CompatibilityZoneParams): CordaFuture> { + compatibilityZone: CompatibilityZoneParams): CordaFuture>> { // Start the registration process for all the notaries together then wait for their responses. return notarySpecs.map { spec -> require(spec.cluster == null) { "Registering distributed notaries not supported" } @@ -518,51 +531,56 @@ class DriverDSLImpl( spec: NotarySpec, rootCert: X509Certificate, compatibilityZone: CompatibilityZoneParams - ): CordaFuture { - return startNodeRegistration(spec.name, rootCert, compatibilityZone.config()).flatMap { config -> - // Node registration only gives us the node CA cert, not the identity cert. That is only created on first - // startup or when the node is told to just generate its node info file. We do that here. - if (startNodesInProcess) { - executorService.fork { - val nodeInfo = Node(config.corda, MOCK_VERSION_INFO, initialiseSerialization = false).generateAndSaveNodeInfo() - NotaryInfo(nodeInfo.legalIdentities[0], spec.validating) - } - } else { - // TODO The config we use here is uses a hardocded p2p port which changes when the node is run proper - // This causes two node info files to be generated. - startOutOfProcessMiniNode(config, arrayOf("generate-node-info")).map { - // Once done we have to read the signed node info file that's been generated - val nodeInfoFile = config.corda.baseDirectory.list { paths -> - paths.filter { it.fileName.toString().startsWith(NodeInfoFilesCopier.NODE_INFO_FILE_NAME_PREFIX) }.findFirst().get() + ): CordaFuture> { + val notaryConfig = mapOf("notary" to mapOf("validating" to spec.validating)) + val parameters = NodeParameters(rpcUsers = spec.rpcUsers, verifierType = spec.verifierType, customOverrides = notaryConfig + notaryCustomOverrides, maximumHeapSize = spec.maximumHeapSize) + return createSchema(createConfig(spec.name, parameters), false).doOnComplete { config -> + startNodeRegistration(config, rootCert, compatibilityZone.config())}.flatMap { config -> + // Node registration only gives us the node CA cert, not the identity cert. That is only created on first + // startup or when the node is told to just generate its node info file. We do that here. + if (startNodesInProcess) { + executorService.fork { + val nodeInfo = Node(config.corda, MOCK_VERSION_INFO, initialiseSerialization = false).generateAndSaveNodeInfo() + Pair(config, NotaryInfo(nodeInfo.legalIdentities[0], spec.validating)) + } + } else { + // TODO The config we use here is uses a hardocded p2p port which changes when the node is run proper + // This causes two node info files to be generated. + startOutOfProcessMiniNode(config, arrayOf("generate-node-info")).map { + // Once done we have to read the signed node info file that's been generated + val nodeInfoFile = config.corda.baseDirectory.list { paths -> + paths.filter { it.fileName.toString().startsWith(NodeInfoFilesCopier.NODE_INFO_FILE_NAME_PREFIX) }.findFirst() + .get() + } + val nodeInfo = nodeInfoFile.readObject().verified() + Pair(config,NotaryInfo(nodeInfo.legalIdentities[0], spec.validating)) } - val nodeInfo = nodeInfoFile.readObject().verified() - NotaryInfo(nodeInfo.legalIdentities[0], spec.validating) } } - } + } private fun generateNodeNames(spec: NotarySpec): List { return (0 until spec.cluster!!.clusterSize).map { spec.name.copy(organisation = "${spec.name.organisation}-$it") } } - private fun startNotaries(localNetworkMap: LocalNetworkMap?, customOverrides: Map): List>> { - return notarySpecs.map { - when (it.cluster) { - null -> startSingleNotary(it, localNetworkMap, customOverrides) + private fun startNotaries(configs: List, localNetworkMap: LocalNetworkMap?, customOverrides: Map): List>> { + return notarySpecs.zip(configs).map { (spec, config) -> + when (spec.cluster) { + null -> startSingleNotary(config, spec, localNetworkMap, customOverrides) is ClusterSpec.Raft, // DummyCluster is used for testing the notary communication path, and it does not matter // which underlying consensus algorithm is used, so we just stick to Raft - is DummyClusterSpec -> startRaftNotaryCluster(it, localNetworkMap) + is DummyClusterSpec -> startRaftNotaryCluster(spec, localNetworkMap) else -> throw IllegalArgumentException("BFT-SMaRt not supported") } } } - private fun startSingleNotary(spec: NotarySpec, localNetworkMap: LocalNetworkMap?, customOverrides: Map): CordaFuture> { + private fun startSingleNotary(config: NodeConfig, spec: NotarySpec, localNetworkMap: LocalNetworkMap?, customOverrides: Map): CordaFuture> { val notaryConfig = mapOf("notary" to mapOf("validating" to spec.validating)) return startRegisteredNode( - spec.name, + config, localNetworkMap, NodeParameters(rpcUsers = spec.rpcUsers, verifierType = spec.verifierType, customOverrides = notaryConfig + customOverrides, maximumHeapSize = spec.maximumHeapSize) ).map { listOf(it) } @@ -585,20 +603,26 @@ class DriverDSLImpl( val nodeNames = generateNodeNames(spec) val clusterAddress = portAllocation.nextHostAndPort() + val firstParams = NodeParameters(rpcUsers = spec.rpcUsers, verifierType = spec.verifierType, customOverrides = notaryConfig(clusterAddress)) + val firstConfig = createSchema(createConfig(nodeNames[0], firstParams), allowHibernateToManageAppSchema) + // Start the first node that will bootstrap the cluster val firstNodeFuture = startRegisteredNode( - nodeNames[0], + firstConfig.getOrThrow(), localNetworkMap, - NodeParameters(rpcUsers = spec.rpcUsers, verifierType = spec.verifierType, customOverrides = notaryConfig(clusterAddress)) + firstParams ) // All other nodes will join the cluster val restNodeFutures = nodeNames.drop(1).map { val nodeAddress = portAllocation.nextHostAndPort() + val params = NodeParameters(rpcUsers = spec.rpcUsers, verifierType = spec.verifierType, customOverrides = notaryConfig(nodeAddress, clusterAddress)) + val config = createSchema(createConfig(it, params), allowHibernateToManageAppSchema) startRegisteredNode( - it, + config.getOrThrow(), localNetworkMap, - NodeParameters(rpcUsers = spec.rpcUsers, verifierType = spec.verifierType, customOverrides = notaryConfig(nodeAddress, clusterAddress)) + params + ) } @@ -663,7 +687,7 @@ class DriverDSLImpl( ) val nodeFuture = if (parameters.startInSameProcess ?: startNodesInProcess) { - val nodeAndThreadFuture = startInProcessNode(executorService, config) + val nodeAndThreadFuture = startInProcessNode(executorService, config, allowHibernateToManageAppSchema) shutdownManager.registerShutdown( nodeAndThreadFuture.map { (node, thread) -> { @@ -689,6 +713,9 @@ class DriverDSLImpl( nodeFuture } else { val debugPort = if (isDebug) debugPortAllocation.nextPort() else null + log.info("StartNodeInternal for ${config.corda.myLegalName.organisation} - calling create schema") + createSchema(config, allowHibernateToManageAppSchema).getOrThrow() + log.info("StartNodeInternal for ${config.corda.myLegalName.organisation} - create schema done") val process = startOutOfProcessNode( config, quasarJarPath, @@ -699,7 +726,10 @@ class DriverDSLImpl( parameters.maximumHeapSize, parameters.logLevelOverride, identifier, - environmentVariables + environmentVariables, + extraCmdLineFlag = listOfNotNull( + if (allowHibernateToManageAppSchema) "--allow-hibernate-to-manage-app-schema" else null + ).toTypedArray() ) // Destroy the child process when the parent exits.This is needed even when `waitForAllNodesToFinish` is @@ -853,7 +883,8 @@ class DriverDSLImpl( private fun startInProcessNode( executorService: ScheduledExecutorService, - config: NodeConfig + config: NodeConfig, + allowHibernateToManageAppSchema: Boolean ): CordaFuture> { val effectiveP2PAddress = config.corda.messagingServerAddress ?: config.corda.p2pAddress return executorService.fork { @@ -864,7 +895,7 @@ class DriverDSLImpl( // Write node.conf writeConfig(config.corda.baseDirectory, "node.conf", config.typesafe.toNodeOnly()) // TODO pass the version in? - val node = InProcessNode(config.corda, MOCK_VERSION_INFO) + val node = InProcessNode(config.corda, MOCK_VERSION_INFO, allowHibernateToManageAppSchema = allowHibernateToManageAppSchema) val nodeInfo = node.start() val nodeWithInfo = NodeWithInfo(node, nodeInfo) val nodeThread = thread(name = config.corda.myLegalName.organisation) { @@ -1241,7 +1272,8 @@ fun genericDriver( cordappsForAllNodes = uncheckedCast(defaultParameters.cordappsForAllNodes), djvmBootstrapSource = defaultParameters.djvmBootstrapSource, djvmCordaSource = defaultParameters.djvmCordaSource, - environmentVariables = defaultParameters.environmentVariables + environmentVariables = defaultParameters.environmentVariables, + allowHibernateToManageAppSchema = defaultParameters.allowHibernateToManageAppSchema ) ) val shutdownHook = addShutdownHook(driverDsl::shutdown) @@ -1339,29 +1371,31 @@ fun internalDriver( djvmBootstrapSource: Path? = null, djvmCordaSource: List = emptyList(), environmentVariables: Map = emptyMap(), + allowHibernateToManageAppSchema: Boolean = true, dsl: DriverDSLImpl.() -> A ): A { return genericDriver( driverDsl = DriverDSLImpl( - portAllocation = portAllocation, - debugPortAllocation = debugPortAllocation, - systemProperties = systemProperties, - driverDirectory = driverDirectory.toAbsolutePath(), - useTestClock = useTestClock, - isDebug = isDebug, - startNodesInProcess = startNodesInProcess, - waitForAllNodesToFinish = waitForAllNodesToFinish, - extraCordappPackagesToScan = extraCordappPackagesToScan, - notarySpecs = notarySpecs, - jmxPolicy = jmxPolicy, - compatibilityZone = compatibilityZone, - networkParameters = networkParameters, - notaryCustomOverrides = notaryCustomOverrides, - inMemoryDB = inMemoryDB, - cordappsForAllNodes = cordappsForAllNodes, - djvmBootstrapSource = djvmBootstrapSource, - djvmCordaSource = djvmCordaSource, - environmentVariables = environmentVariables + portAllocation = portAllocation, + debugPortAllocation = debugPortAllocation, + systemProperties = systemProperties, + driverDirectory = driverDirectory.toAbsolutePath(), + useTestClock = useTestClock, + isDebug = isDebug, + startNodesInProcess = startNodesInProcess, + waitForAllNodesToFinish = waitForAllNodesToFinish, + extraCordappPackagesToScan = extraCordappPackagesToScan, + notarySpecs = notarySpecs, + jmxPolicy = jmxPolicy, + compatibilityZone = compatibilityZone, + networkParameters = networkParameters, + notaryCustomOverrides = notaryCustomOverrides, + inMemoryDB = inMemoryDB, + cordappsForAllNodes = cordappsForAllNodes, + djvmBootstrapSource = djvmBootstrapSource, + djvmCordaSource = djvmCordaSource, + environmentVariables = environmentVariables, + allowHibernateToManageAppSchema = allowHibernateToManageAppSchema ), coerce = { it }, dsl = dsl diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt index aea0e9d5d0..5c3c3df0ce 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt @@ -286,7 +286,8 @@ open class InternalMockNetwork(cordappPackages: List = emptyList(), args.version, mockFlowManager, args.network.getServerThread(args.id), - args.network.busyLatch + args.network.busyLatch, + allowHibernateToManageAppSchema = true ) { companion object { private val staticLog = contextLogger() @@ -317,6 +318,8 @@ open class InternalMockNetwork(cordappPackages: List = emptyList(), } } + override val runMigrationScripts: Boolean = true + val mockNet = args.network val id = args.id diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt index 662766665b..8690310e4f 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt @@ -152,7 +152,12 @@ constructor(private val cordappPackages: List = emptyList(), private val } } -class InProcessNode(configuration: NodeConfiguration, versionInfo: VersionInfo, flowManager: FlowManager = NodeFlowManager(configuration.flowOverrides)) : Node(configuration, versionInfo, false, flowManager = flowManager) { +class InProcessNode( + configuration: NodeConfiguration, + versionInfo: VersionInfo, + flowManager: FlowManager = NodeFlowManager(configuration.flowOverrides), + allowHibernateToManageAppSchema: Boolean = true) : Node(configuration, versionInfo, false, flowManager = flowManager, allowHibernateToManageAppSchema = allowHibernateToManageAppSchema) { + override val runMigrationScripts: Boolean = true override fun start(): NodeInfo { assertFalse(isInvalidJavaVersion(), "You are using a version of Java that is not supported (${SystemUtils.JAVA_VERSION}). Please upgrade to the latest version of Java 8.") return super.start() diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt index 84e7091d1f..5a0448b5f9 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt @@ -163,6 +163,7 @@ fun RPCSecurityManagerImpl.Companion.fromUserList(id: AuthServiceId, users: List /** * Convenience method for configuring a database for some tests. */ +@Suppress("LongParameterList") fun configureDatabase(hikariProperties: Properties, databaseConfig: DatabaseConfig, wellKnownPartyFromX500Name: (CordaX500Name) -> Party?, @@ -170,9 +171,19 @@ fun configureDatabase(hikariProperties: Properties, schemaService: SchemaService = NodeSchemaService(), internalSchemas: Set = NodeSchemaService().internalSchemas(), cacheFactory: NamedCacheFactory = TestingNamedCacheFactory(), - ourName: CordaX500Name = TestIdentity(ALICE_NAME, 70).name): CordaPersistence { - val persistence = createCordaPersistence(databaseConfig, wellKnownPartyFromX500Name, wellKnownPartyFromAnonymous, schemaService, hikariProperties, cacheFactory, null) - persistence.startHikariPool(hikariProperties, databaseConfig, internalSchemas, ourName = ourName) + ourName: CordaX500Name = TestIdentity(ALICE_NAME, 70).name, + runMigrationScripts: Boolean = true, + allowHibernateToManageAppSchema: Boolean = true): CordaPersistence { + val persistence = createCordaPersistence( + databaseConfig, + wellKnownPartyFromX500Name, + wellKnownPartyFromAnonymous, + schemaService, + hikariProperties, + cacheFactory, + null, + allowHibernateToManageAppSchema) + persistence.startHikariPool(hikariProperties, internalSchemas, ourName = ourName, runMigrationScripts = runMigrationScripts) return persistence } From 4a54ae5eb9f4b41f5d36a425159874a5597adc89 Mon Sep 17 00:00:00 2001 From: Christian Sailer Date: Fri, 5 Jun 2020 12:11:45 +0100 Subject: [PATCH 02/45] ENT-4493 schema migration refactor (#6313) ENT-4493 Refactor SchemaMigration so it can be open harmonised with Enterprise and can be customised. --- .../persistence/LiquibaseDatabaseFactory.kt | 8 + .../LiquibaseDatabaseFactoryImpl.kt | 11 ++ .../internal/persistence/SchemaMigration.kt | 144 ++++++++++-------- 3 files changed, 97 insertions(+), 66 deletions(-) create mode 100644 node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/LiquibaseDatabaseFactory.kt create mode 100644 node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/LiquibaseDatabaseFactoryImpl.kt diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/LiquibaseDatabaseFactory.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/LiquibaseDatabaseFactory.kt new file mode 100644 index 0000000000..ba67c35946 --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/LiquibaseDatabaseFactory.kt @@ -0,0 +1,8 @@ +package net.corda.nodeapi.internal.persistence + +import liquibase.database.Database +import liquibase.database.jvm.JdbcConnection + +interface LiquibaseDatabaseFactory { + fun getLiquibaseDatabase(conn: JdbcConnection): Database +} \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/LiquibaseDatabaseFactoryImpl.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/LiquibaseDatabaseFactoryImpl.kt new file mode 100644 index 0000000000..317f1e6edf --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/LiquibaseDatabaseFactoryImpl.kt @@ -0,0 +1,11 @@ +package net.corda.nodeapi.internal.persistence + +import liquibase.database.Database +import liquibase.database.DatabaseFactory +import liquibase.database.jvm.JdbcConnection + +class LiquibaseDatabaseFactoryImpl : LiquibaseDatabaseFactory { + override fun getLiquibaseDatabase(conn: JdbcConnection): Database { + return DatabaseFactory.getInstance().findCorrectDatabaseImplementation(conn) + } +} \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/SchemaMigration.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/SchemaMigration.kt index ce592d95da..733c9ecf94 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/SchemaMigration.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/SchemaMigration.kt @@ -4,25 +4,25 @@ import com.fasterxml.jackson.databind.ObjectMapper import liquibase.Contexts import liquibase.LabelExpression import liquibase.Liquibase -import liquibase.database.Database -import liquibase.database.DatabaseFactory import liquibase.database.jvm.JdbcConnection +import liquibase.exception.LiquibaseException import liquibase.resource.ClassLoaderResourceAccessor import net.corda.core.identity.CordaX500Name -import net.corda.nodeapi.internal.MigrationHelpers.getMigrationResource import net.corda.core.schemas.MappedSchema import net.corda.core.utilities.contextLogger +import net.corda.nodeapi.internal.MigrationHelpers.getMigrationResource import net.corda.nodeapi.internal.cordapp.CordappLoader import java.io.ByteArrayInputStream import java.io.InputStream import java.nio.file.Path +import java.sql.Connection import java.sql.Statement -import javax.sql.DataSource import java.util.concurrent.locks.ReentrantLock +import javax.sql.DataSource import kotlin.concurrent.withLock // Migrate the database to the current version, using liquibase. -class SchemaMigration( +open class SchemaMigration( val schemas: Set, val dataSource: DataSource, cordappLoader: CordappLoader? = null, @@ -33,14 +33,16 @@ class SchemaMigration( private val ourName: CordaX500Name? = null, // This parameter forces an error to be thrown if there are missing migrations. When using H2, Hibernate will automatically create schemas where they are // missing, so no need to throw unless you're specifically testing whether all the migrations are present. - private val forceThrowOnMissingMigration: Boolean = false) { + private val forceThrowOnMissingMigration: Boolean = false, + protected val databaseFactory: LiquibaseDatabaseFactory = LiquibaseDatabaseFactoryImpl()) { companion object { private val logger = contextLogger() const val NODE_BASE_DIR_KEY = "liquibase.nodeDaseDir" const val NODE_X500_NAME = "liquibase.nodeName" val loader = ThreadLocal() - private val mutex = ReentrantLock() + @JvmStatic + protected val mutex = ReentrantLock() } init { @@ -49,25 +51,54 @@ class SchemaMigration( private val classLoader = cordappLoader?.appClassLoader ?: Thread.currentThread().contextClassLoader - /** + /** * Will run the Liquibase migration on the actual database. */ - fun runMigration(existingCheckpoints: Boolean) { - migrateOlderDatabaseToUseLiquibase(existingCheckpoints) - doRunMigration(run = true, check = false, existingCheckpoints = existingCheckpoints) - } + fun runMigration(existingCheckpoints: Boolean) { + migrateOlderDatabaseToUseLiquibase(existingCheckpoints) + val resourcesAndSourceInfo = prepareResources() + + // current version of Liquibase appears to be non-threadsafe + // this is apparent when multiple in-process nodes are all running migrations simultaneously + mutex.withLock { + dataSource.connection.use { connection -> + val (runner, _, shouldBlockOnCheckpoints) = prepareRunner(connection, resourcesAndSourceInfo) + if (shouldBlockOnCheckpoints && existingCheckpoints) + throw CheckpointsException() + try { + runner.update(Contexts().toString()) + } catch (exp: LiquibaseException) { + throw DatabaseMigrationException(exp.message, exp) + } + } + } + } /** * Ensures that the database is up to date with the latest migration changes. */ - fun checkState() = doRunMigration(run = false, check = true) + fun checkState() { + val resourcesAndSourceInfo = prepareResources() - /** Create a resourse accessor that aggregates the changelogs included in the schemas into one dynamic stream. */ - private class CustomResourceAccessor(val dynamicInclude: String, val changelogList: List, classLoader: ClassLoader) : ClassLoaderResourceAccessor(classLoader) { + // current version of Liquibase appears to be non-threadsafe + // this is apparent when multiple in-process nodes are all running migrations simultaneously + mutex.withLock { + dataSource.connection.use { connection -> + val (_, changeToRunCount, _) = prepareRunner(connection, resourcesAndSourceInfo) + if (changeToRunCount > 0) + throw OutstandingDatabaseChangesException(changeToRunCount) + } + } + } + + /** Create a resource accessor that aggregates the changelogs included in the schemas into one dynamic stream. */ + protected class CustomResourceAccessor(val dynamicInclude: String, val changelogList: List, classLoader: ClassLoader) : + ClassLoaderResourceAccessor(classLoader) { override fun getResourcesAsStream(path: String): Set { if (path == dynamicInclude) { // Create a map in Liquibase format including all migration files. - val includeAllFiles = mapOf("databaseChangeLog" to changelogList.filter { it != null }.map { file -> mapOf("include" to mapOf("file" to file)) }) + val includeAllFiles = mapOf("databaseChangeLog" + to changelogList.filterNotNull().map { file -> mapOf("include" to mapOf("file" to file)) }) // Transform it to json. val includeAllFilesJson = ObjectMapper().writeValueAsBytes(includeAllFiles) @@ -87,59 +118,40 @@ class SchemaMigration( null } - private fun doRunMigration( - run: Boolean, - check: Boolean, - existingCheckpoints: Boolean? = null - ) { + // Virtual file name of the changelog that includes all schemas. + val dynamicInclude = "master.changelog.json" - // Virtual file name of the changelog that includes all schemas. - val dynamicInclude = "master.changelog.json" - - dataSource.connection.use { connection -> - - // Collect all changelog files referenced in the included schemas. - val changelogList = schemas.mapNotNull { mappedSchema -> - val resource = getMigrationResource(mappedSchema, classLoader) - when { - resource != null -> resource - // Corda OS FinanceApp in v3 has no Liquibase script, so no error is raised - (mappedSchema::class.qualifiedName == "net.corda.finance.schemas.CashSchemaV1" || mappedSchema::class.qualifiedName == "net.corda.finance.schemas.CommercialPaperSchemaV1") && mappedSchema.migrationResource == null -> null - else -> logOrThrowMigrationError(mappedSchema) - } - } - - val path = currentDirectory?.toString() - if (path != null) { - System.setProperty(NODE_BASE_DIR_KEY, path) // base dir for any custom change set which may need to load a file (currently AttachmentVersionNumberMigration) - } - if (ourName != null) { - System.setProperty(NODE_X500_NAME, ourName.toString()) - } - val customResourceAccessor = CustomResourceAccessor(dynamicInclude, changelogList, classLoader) - checkResourcesInClassPath(changelogList) - - // current version of Liquibase appears to be non-threadsafe - // this is apparent when multiple in-process nodes are all running migrations simultaneously - mutex.withLock { - val liquibase = Liquibase(dynamicInclude, customResourceAccessor, getLiquibaseDatabase(JdbcConnection(connection))) - - val unRunChanges = liquibase.listUnrunChangeSets(Contexts(), LabelExpression()) - - when { - (run && !check) && (unRunChanges.isNotEmpty() && existingCheckpoints!!) -> throw CheckpointsException() // Do not allow database migration when there are checkpoints - run && !check -> liquibase.update(Contexts()) - check && !run && unRunChanges.isNotEmpty() -> throw OutstandingDatabaseChangesException(unRunChanges.size) - check && !run -> { - } // Do nothing will be interpreted as "check succeeded" - else -> throw IllegalStateException("Invalid usage.") - } + protected fun prepareResources(): List> { + // Collect all changelog files referenced in the included schemas. + val changelogList = schemas.mapNotNull { mappedSchema -> + val resource = getMigrationResource(mappedSchema, classLoader) + when { + resource != null -> resource + // Corda OS FinanceApp in v3 has no Liquibase script, so no error is raised + (mappedSchema::class.qualifiedName == "net.corda.finance.schemas.CashSchemaV1" || mappedSchema::class.qualifiedName == "net.corda.finance.schemas.CommercialPaperSchemaV1") && mappedSchema.migrationResource == null -> null + else -> logOrThrowMigrationError(mappedSchema) } } + + val path = currentDirectory?.toString() + if (path != null) { + System.setProperty(NODE_BASE_DIR_KEY, path) // base dir for any custom change set which may need to load a file (currently AttachmentVersionNumberMigration) + } + if (ourName != null) { + System.setProperty(NODE_X500_NAME, ourName.toString()) + } + val customResourceAccessor = CustomResourceAccessor(dynamicInclude, changelogList, classLoader) + checkResourcesInClassPath(changelogList) + return listOf(Pair(customResourceAccessor, "")) } - private fun getLiquibaseDatabase(conn: JdbcConnection): Database { - return DatabaseFactory.getInstance().findCorrectDatabaseImplementation(conn) + protected fun prepareRunner(connection: Connection, + resourcesAndSourceInfo: List>): Triple { + require(resourcesAndSourceInfo.size == 1) + val liquibase = Liquibase(dynamicInclude, resourcesAndSourceInfo.single().first, databaseFactory.getLiquibaseDatabase(JdbcConnection(connection))) + + val unRunChanges = liquibase.listUnrunChangeSets(Contexts(), LabelExpression()) + return Triple(liquibase, unRunChanges.size, !unRunChanges.isEmpty()) } /** For existing database created before verions 4.0 add Liquibase support - creates DATABASECHANGELOG and DATABASECHANGELOGLOCK tables and marks changesets as executed. */ @@ -219,7 +231,7 @@ class SchemaMigration( checkResourcesInClassPath(preV4Baseline) dataSource.connection.use { connection -> val customResourceAccessor = CustomResourceAccessor(dynamicInclude, preV4Baseline, classLoader) - val liquibase = Liquibase(dynamicInclude, customResourceAccessor, getLiquibaseDatabase(JdbcConnection(connection))) + val liquibase = Liquibase(dynamicInclude, customResourceAccessor, databaseFactory.getLiquibaseDatabase(JdbcConnection(connection))) liquibase.changeLogSync(Contexts(), LabelExpression()) } } @@ -235,7 +247,7 @@ class SchemaMigration( } } -open class DatabaseMigrationException(message: String) : IllegalArgumentException(message) { +open class DatabaseMigrationException(message: String?, cause: Throwable? = null) : IllegalArgumentException(message, cause) { override val message: String = super.message!! } From f1126226a8a94e0f5440a1108f194b9919152226 Mon Sep 17 00:00:00 2001 From: Christian Sailer Date: Fri, 12 Jun 2020 20:54:36 +0100 Subject: [PATCH 03/45] Fix config tests (remove tx isolation level from config files) --- node/src/test/resources/working-config-no-cordapps.conf | 1 - 1 file changed, 1 deletion(-) diff --git a/node/src/test/resources/working-config-no-cordapps.conf b/node/src/test/resources/working-config-no-cordapps.conf index 419b866013..89ae6036a2 100644 --- a/node/src/test/resources/working-config-no-cordapps.conf +++ b/node/src/test/resources/working-config-no-cordapps.conf @@ -11,7 +11,6 @@ dataSourceProperties = { dataSource.password = "" } database = { - transactionIsolationLevel = "REPEATABLE_READ" exportHibernateJMXStatistics = "false" } p2pAddress = "localhost:2233" From 836dd559e802ba9f64e091d08547f8f5469e32d7 Mon Sep 17 00:00:00 2001 From: Christian Sailer Date: Mon, 15 Jun 2020 15:52:31 +0100 Subject: [PATCH 04/45] ENT-5316 split schema migration * ENT-5273 Split schema migration into separate core and app schema migration, with separate command line flags --- constants.properties | 2 +- .../flows/ReceiveFinalityFlowTest.kt | 4 +- .../persistence/MissingSchemaMigrationTest.kt | 16 ++-- .../internal/network/NetworkBootstrapper.kt | 3 +- .../internal/persistence/SchemaMigration.kt | 62 +++++--------- ...owCheckpointVersionNodeStartupCheckTest.kt | 3 +- .../persistence/NodeStatePersistenceTests.kt | 2 +- .../StatemachineErrorHandlingTest.kt | 8 +- .../corda/node/CordappScanningDriverTest.kt | 3 +- .../net/corda/node/NodeConfigParsingTests.kt | 12 ++- .../net/corda/node/internal/AbstractNode.kt | 82 ++++++++++++++----- .../kotlin/net/corda/node/internal/Node.kt | 7 +- .../subcommands/RunMigrationScriptsCli.kt | 15 +++- .../node/services/schema/NodeSchemaService.kt | 7 +- .../services/persistence/DbMapDeadlockTest.kt | 7 +- samples/attachment-demo/build.gradle | 1 + samples/bank-of-corda-demo/build.gradle | 1 + samples/cordapp-configuration/build.gradle | 1 + samples/irs-demo/cordapp/build.gradle | 1 + samples/network-verifier/build.gradle | 1 + samples/notary-demo/build.gradle | 1 + samples/simm-valuation-demo/build.gradle | 1 + samples/trader-demo/build.gradle | 1 + .../net/corda/testing/node/MockServices.kt | 4 +- .../testing/node/internal/DriverDSLImpl.kt | 2 + .../node/internal/InternalMockNetwork.kt | 8 +- .../testing/internal/InternalTestUtils.kt | 19 +++-- 27 files changed, 170 insertions(+), 104 deletions(-) diff --git a/constants.properties b/constants.properties index 2b2775389d..b4a092855f 100644 --- a/constants.properties +++ b/constants.properties @@ -4,7 +4,7 @@ cordaVersion=4.6 versionSuffix=SNAPSHOT -gradlePluginsVersion=5.0.9 +gradlePluginsVersion=5.0.10 kotlinVersion=1.2.71 java8MinUpdateVersion=171 # ***************************************************************# diff --git a/core-tests/src/test/kotlin/net/corda/coretests/flows/ReceiveFinalityFlowTest.kt b/core-tests/src/test/kotlin/net/corda/coretests/flows/ReceiveFinalityFlowTest.kt index 6c50e4e2a2..4f1406711a 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/flows/ReceiveFinalityFlowTest.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/flows/ReceiveFinalityFlowTest.kt @@ -56,7 +56,9 @@ class ReceiveFinalityFlowTest { bob.assertFlowSentForObservationDueToConstraintError(paymentReceiverId) // Restart Bob with the contracts CorDapp so that it can recover from the error - bob = mockNet.restartNode(bob, parameters = InternalMockNodeParameters(additionalCordapps = listOf(FINANCE_CONTRACTS_CORDAPP))) + bob = mockNet.restartNode(bob, + parameters = InternalMockNodeParameters(additionalCordapps = listOf(FINANCE_CONTRACTS_CORDAPP)), + nodeFactory = { args -> InternalMockNetwork.MockNode(args, allowAppSchemaUpgradeWithCheckpoints = true) }) mockNet.runNetwork() assertThat(bob.services.getCashBalance(GBP)).isEqualTo(100.POUNDS) } diff --git a/node-api-tests/src/test/kotlin/net/corda/nodeapitests/internal/persistence/MissingSchemaMigrationTest.kt b/node-api-tests/src/test/kotlin/net/corda/nodeapitests/internal/persistence/MissingSchemaMigrationTest.kt index 78d30aa2b6..7c2ce493ca 100644 --- a/node-api-tests/src/test/kotlin/net/corda/nodeapitests/internal/persistence/MissingSchemaMigrationTest.kt +++ b/node-api-tests/src/test/kotlin/net/corda/nodeapitests/internal/persistence/MissingSchemaMigrationTest.kt @@ -39,24 +39,21 @@ class MissingSchemaMigrationTest { dataSource = DataSourceFactory.createDataSource(hikariProperties) } - private fun createSchemaMigration(schemasToMigrate: Set, forceThrowOnMissingMigration: Boolean): SchemaMigration { - return SchemaMigration(schemasToMigrate, dataSource, null, null, - TestIdentity(ALICE_NAME, 70).name, forceThrowOnMissingMigration) - } + private fun schemaMigration() = SchemaMigration(dataSource, null, null, + TestIdentity(ALICE_NAME, 70).name) + @Test(timeout=300_000) fun `test that an error is thrown when forceThrowOnMissingMigration is set and a mapped schema is missing a migration`() { assertThatThrownBy { - createSchemaMigration(setOf(GoodSchema), true) - .runMigration(dataSource.connection.use { DBCheckpointStorage.getCheckpointCount(it) != 0L }) + schemaMigration().runMigration(dataSource.connection.use { DBCheckpointStorage.getCheckpointCount(it) != 0L }, setOf(GoodSchema), true) }.isInstanceOf(MissingMigrationException::class.java) } @Test(timeout=300_000) fun `test that an error is not thrown when forceThrowOnMissingMigration is not set and a mapped schema is missing a migration`() { assertDoesNotThrow { - createSchemaMigration(setOf(GoodSchema), false) - .runMigration(dataSource.connection.use { DBCheckpointStorage.getCheckpointCount(it) != 0L }) + schemaMigration().runMigration(dataSource.connection.use { DBCheckpointStorage.getCheckpointCount(it) != 0L }, setOf(GoodSchema), false) } } @@ -64,8 +61,7 @@ class MissingSchemaMigrationTest { fun `test that there are no missing migrations for the node`() { assertDoesNotThrow("This test failure indicates " + "a new table has been added to the node without the appropriate migration scripts being present") { - createSchemaMigration(NodeSchemaService().internalSchemas(), false) - .runMigration(dataSource.connection.use { DBCheckpointStorage.getCheckpointCount(it) != 0L }) + schemaMigration().runMigration(dataSource.connection.use { DBCheckpointStorage.getCheckpointCount(it) != 0L }, NodeSchemaService().internalSchemas, true) } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt index 3ec783ec99..7e63bda8e8 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt @@ -79,7 +79,8 @@ constructor(private val initSerEnv: Boolean, Paths.get(System.getProperty("java.home"), "bin", "java").toString(), "-jar", "corda.jar", - "run-migration-scripts" + "run-migration-scripts", + "--core-schemas" ) private const val LOGS_DIR_NAME = "logs" diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/SchemaMigration.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/SchemaMigration.kt index 733c9ecf94..cd181b0bb4 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/SchemaMigration.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/SchemaMigration.kt @@ -16,14 +16,12 @@ import java.io.ByteArrayInputStream import java.io.InputStream import java.nio.file.Path import java.sql.Connection -import java.sql.Statement import java.util.concurrent.locks.ReentrantLock import javax.sql.DataSource import kotlin.concurrent.withLock // Migrate the database to the current version, using liquibase. open class SchemaMigration( - val schemas: Set, val dataSource: DataSource, cordappLoader: CordappLoader? = null, private val currentDirectory: Path?, @@ -31,9 +29,6 @@ open class SchemaMigration( // its copy of the identity service. It is passed through using a system property. When multiple identity support is added, this will need // reworking so that multiple identities can be passed to the migration. private val ourName: CordaX500Name? = null, - // This parameter forces an error to be thrown if there are missing migrations. When using H2, Hibernate will automatically create schemas where they are - // missing, so no need to throw unless you're specifically testing whether all the migrations are present. - private val forceThrowOnMissingMigration: Boolean = false, protected val databaseFactory: LiquibaseDatabaseFactory = LiquibaseDatabaseFactoryImpl()) { companion object { @@ -53,10 +48,14 @@ open class SchemaMigration( /** * Will run the Liquibase migration on the actual database. + * @param existingCheckpoints Whether checkpoints exist that would prohibit running a migration + * @param schemas The set of MappedSchemas to check + * @param forceThrowOnMissingMigration throws an exception if a mapped schema is missing the migration resource. Can be set to false + * when allowing hibernate to create missing schemas in dev or tests. */ - fun runMigration(existingCheckpoints: Boolean) { - migrateOlderDatabaseToUseLiquibase(existingCheckpoints) - val resourcesAndSourceInfo = prepareResources() + fun runMigration(existingCheckpoints: Boolean, schemas: Set, forceThrowOnMissingMigration: Boolean) { + migrateOlderDatabaseToUseLiquibase(existingCheckpoints, schemas) + val resourcesAndSourceInfo = prepareResources(schemas, forceThrowOnMissingMigration) // current version of Liquibase appears to be non-threadsafe // this is apparent when multiple in-process nodes are all running migrations simultaneously @@ -76,9 +75,12 @@ open class SchemaMigration( /** * Ensures that the database is up to date with the latest migration changes. + * @param schemas The set of MappedSchemas to check + * @param forceThrowOnMissingMigration throws an exception if a mapped schema is missing the migration resource. Can be set to false + * when allowing hibernate to create missing schemas in dev or tests. */ - fun checkState() { - val resourcesAndSourceInfo = prepareResources() + fun checkState(schemas: Set, forceThrowOnMissingMigration: Boolean) { + val resourcesAndSourceInfo = prepareResources(schemas, forceThrowOnMissingMigration) // current version of Liquibase appears to be non-threadsafe // this is apparent when multiple in-process nodes are all running migrations simultaneously @@ -110,7 +112,7 @@ open class SchemaMigration( } } - private fun logOrThrowMigrationError(mappedSchema: MappedSchema): String? = + private fun logOrThrowMigrationError(mappedSchema: MappedSchema, forceThrowOnMissingMigration: Boolean): String? = if (forceThrowOnMissingMigration) { throw MissingMigrationException(mappedSchema) } else { @@ -121,15 +123,13 @@ open class SchemaMigration( // Virtual file name of the changelog that includes all schemas. val dynamicInclude = "master.changelog.json" - protected fun prepareResources(): List> { + protected fun prepareResources(schemas: Set, forceThrowOnMissingMigration: Boolean): List> { // Collect all changelog files referenced in the included schemas. val changelogList = schemas.mapNotNull { mappedSchema -> val resource = getMigrationResource(mappedSchema, classLoader) when { resource != null -> resource - // Corda OS FinanceApp in v3 has no Liquibase script, so no error is raised - (mappedSchema::class.qualifiedName == "net.corda.finance.schemas.CashSchemaV1" || mappedSchema::class.qualifiedName == "net.corda.finance.schemas.CommercialPaperSchemaV1") && mappedSchema.migrationResource == null -> null - else -> logOrThrowMigrationError(mappedSchema) + else -> logOrThrowMigrationError(mappedSchema, forceThrowOnMissingMigration) } } @@ -155,21 +155,8 @@ open class SchemaMigration( } /** For existing database created before verions 4.0 add Liquibase support - creates DATABASECHANGELOG and DATABASECHANGELOGLOCK tables and marks changesets as executed. */ - private fun migrateOlderDatabaseToUseLiquibase(existingCheckpoints: Boolean): Boolean { - val isFinanceAppWithLiquibase = schemas.any { schema -> - (schema::class.qualifiedName == "net.corda.finance.schemas.CashSchemaV1" - || schema::class.qualifiedName == "net.corda.finance.schemas.CommercialPaperSchemaV1") - && schema.migrationResource != null - } - val noLiquibaseEntryLogForFinanceApp: (Statement) -> Boolean = { - it.execute("SELECT COUNT(*) FROM DATABASECHANGELOG WHERE FILENAME IN ('migration/cash.changelog-init.xml','migration/commercial-paper.changelog-init.xml')") - if (it.resultSet.next()) - it.resultSet.getInt(1) == 0 - else - true - } - - val (isExistingDBWithoutLiquibase, isFinanceAppWithLiquibaseNotMigrated) = dataSource.connection.use { + private fun migrateOlderDatabaseToUseLiquibase(existingCheckpoints: Boolean, schemas: Set): Boolean { + val isExistingDBWithoutLiquibase = dataSource.connection.use { val existingDatabase = it.metaData.getTables(null, null, "NODE%", null).next() // Lower case names for PostgreSQL @@ -179,12 +166,7 @@ open class SchemaMigration( // Lower case names for PostgreSQL || it.metaData.getTables(null, null, "databasechangelog%", null).next() - val isFinanceAppWithLiquibaseNotMigrated = isFinanceAppWithLiquibase // If Finance App is pre v4.0 then no need to migrate it so no need to check. - && existingDatabase - && (!hasLiquibase // Migrate as other tables. - || (hasLiquibase && it.createStatement().use { noLiquibaseEntryLogForFinanceApp(it) })) // If Liquibase is already in the database check if Finance App schema log is missing. - - Pair(existingDatabase && !hasLiquibase, isFinanceAppWithLiquibaseNotMigrated) + existingDatabase && !hasLiquibase } if (isExistingDBWithoutLiquibase && existingCheckpoints) @@ -219,12 +201,6 @@ open class SchemaMigration( preV4Baseline.addAll(listOf("migration/notary-bft-smart.changelog-init.xml", "migration/notary-bft-smart.changelog-v1.xml")) } - if (isFinanceAppWithLiquibaseNotMigrated) { - preV4Baseline.addAll(listOf("migration/cash.changelog-init.xml", - "migration/cash.changelog-v1.xml", - "migration/commercial-paper.changelog-init.xml", - "migration/commercial-paper.changelog-v1.xml")) - } if (preV4Baseline.isNotEmpty()) { val dynamicInclude = "master.changelog.json" // Virtual file name of the changelog that includes all schemas. @@ -235,7 +211,7 @@ open class SchemaMigration( liquibase.changeLogSync(Contexts(), LabelExpression()) } } - return isExistingDBWithoutLiquibase || isFinanceAppWithLiquibaseNotMigrated + return isExistingDBWithoutLiquibase } private fun checkResourcesInClassPath(resources: List) { diff --git a/node/src/integration-test-slow/kotlin/net/corda/node/flows/FlowCheckpointVersionNodeStartupCheckTest.kt b/node/src/integration-test-slow/kotlin/net/corda/node/flows/FlowCheckpointVersionNodeStartupCheckTest.kt index d6abe718f1..f133881fa0 100644 --- a/node/src/integration-test-slow/kotlin/net/corda/node/flows/FlowCheckpointVersionNodeStartupCheckTest.kt +++ b/node/src/integration-test-slow/kotlin/net/corda/node/flows/FlowCheckpointVersionNodeStartupCheckTest.kt @@ -37,7 +37,8 @@ class FlowCheckpointVersionNodeStartupCheckTest { startNodesInProcess = false, inMemoryDB = false, // Ensure database is persisted between node restarts so we can keep suspended flows cordappsForAllNodes = emptyList(), - notarySpecs = emptyList() + notarySpecs = emptyList(), + allowHibernateToManageAppSchema = false )) { createSuspendedFlowInBob() val cordappsDir = baseDirectory(BOB_NAME) / "cordapps" diff --git a/node/src/integration-test-slow/kotlin/net/corda/node/persistence/NodeStatePersistenceTests.kt b/node/src/integration-test-slow/kotlin/net/corda/node/persistence/NodeStatePersistenceTests.kt index a05e93bffa..396126af91 100644 --- a/node/src/integration-test-slow/kotlin/net/corda/node/persistence/NodeStatePersistenceTests.kt +++ b/node/src/integration-test-slow/kotlin/net/corda/node/persistence/NodeStatePersistenceTests.kt @@ -86,7 +86,7 @@ class NodeStatePersistenceTests { nodeName }() - val nodeHandle = startNode(providedName = nodeName, rpcUsers = listOf(user), customOverrides = mapOf("devMode" to "false")).getOrThrow() + val nodeHandle = startNode(providedName = nodeName, rpcUsers = listOf(user)).getOrThrow() val result = CordaRPCClient(nodeHandle.rpcAddress).start(user.username, user.password).use { val page = it.proxy.vaultQuery(MessageState::class.java) page.states.singleOrNull() diff --git a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineErrorHandlingTest.kt b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineErrorHandlingTest.kt index 7ed4ce5325..20b85b98c1 100644 --- a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineErrorHandlingTest.kt +++ b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineErrorHandlingTest.kt @@ -85,10 +85,10 @@ abstract class StatemachineErrorHandlingTest { internal fun getBytemanOutput(nodeHandle: NodeHandle): List { return nodeHandle.baseDirectory - .list() - .first { it.toString().contains("net.corda.node.Corda") && it.toString().contains("stdout.log") } - .readAllLines() - } + .list() + .filter { it.toString().contains("net.corda.node.Corda") && it.toString().contains("stdout.log") } + .flatMap { it.readAllLines() } + } @StartableByRPC @InitiatingFlow diff --git a/node/src/integration-test/kotlin/net/corda/node/CordappScanningDriverTest.kt b/node/src/integration-test/kotlin/net/corda/node/CordappScanningDriverTest.kt index b594b1e623..1433fbce80 100644 --- a/node/src/integration-test/kotlin/net/corda/node/CordappScanningDriverTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/CordappScanningDriverTest.kt @@ -15,6 +15,7 @@ import net.corda.testing.core.singleIdentity import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.driver import net.corda.testing.node.User +import net.corda.testing.node.internal.enclosedCordapp import org.assertj.core.api.Assertions.assertThat import org.junit.Test @@ -23,7 +24,7 @@ class CordappScanningDriverTest { fun `sub-classed initiated flow pointing to the same initiating flow as its super-class`() { val user = User("u", "p", setOf(startFlow())) // The driver will automatically pick up the annotated flows below - driver(DriverParameters(notarySpecs = emptyList())) { + driver(DriverParameters(notarySpecs = emptyList(), cordappsForAllNodes = listOf(enclosedCordapp()))) { val (alice, bob) = listOf( startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)), startNode(providedName = BOB_NAME)).transpose().getOrThrow() diff --git a/node/src/integration-test/kotlin/net/corda/node/NodeConfigParsingTests.kt b/node/src/integration-test/kotlin/net/corda/node/NodeConfigParsingTests.kt index fd2f7d7507..5854e61fdd 100644 --- a/node/src/integration-test/kotlin/net/corda/node/NodeConfigParsingTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/NodeConfigParsingTests.kt @@ -21,7 +21,8 @@ class NodeConfigParsingTests { driver(DriverParameters( environmentVariables = mapOf("corda_sshd_port" to sshPort.toString()), startNodesInProcess = false, - portAllocation = portAllocator)) { + portAllocation = portAllocator, + cordappsForAllNodes = emptyList())) { val hasSsh = startNode().get() .logFile() .readLines() @@ -39,7 +40,8 @@ class NodeConfigParsingTests { driver(DriverParameters( environmentVariables = mapOf("CORDA_sshd_port" to sshPort.toString()), startNodesInProcess = false, - portAllocation = portAllocator)) { + portAllocation = portAllocator, + cordappsForAllNodes = emptyList())) { val hasSsh = startNode().get() .logFile() .readLines() @@ -58,7 +60,8 @@ class NodeConfigParsingTests { environmentVariables = mapOf("CORDA.sshd.port" to sshPort.toString(), "corda.devMode" to true.toString()), startNodesInProcess = false, - portAllocation = portAllocator)) { + portAllocation = portAllocator, + cordappsForAllNodes = emptyList())) { val hasSsh = startNode(NodeParameters()).get() .logFile() .readLines() @@ -95,7 +98,8 @@ class NodeConfigParsingTests { "corda_bad_key" to "2077"), startNodesInProcess = false, portAllocation = portAllocator, - notarySpecs = emptyList())) { + notarySpecs = emptyList(), + cordappsForAllNodes = emptyList())) { val hasWarning = startNode() .getOrThrow() diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 64be5c3691..8224ba918b 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -172,10 +172,10 @@ import org.apache.activemq.artemis.utils.ReusableLatch import org.jolokia.jvmagent.JolokiaServer import org.jolokia.jvmagent.JolokiaServerConfig import org.slf4j.Logger +import org.slf4j.LoggerFactory import rx.Scheduler import java.io.IOException import java.lang.reflect.InvocationTargetException -import java.nio.file.Path import java.security.KeyPair import java.security.KeyStoreException import java.security.cert.X509Certificate @@ -184,7 +184,7 @@ import java.sql.Savepoint import java.time.Clock import java.time.Duration import java.time.format.DateTimeParseException -import java.util.Properties +import java.util.* import java.util.concurrent.ExecutorService import java.util.concurrent.Executors import java.util.concurrent.LinkedBlockingQueue @@ -194,6 +194,32 @@ import java.util.concurrent.TimeUnit.MINUTES import java.util.concurrent.TimeUnit.SECONDS import java.util.function.Consumer import javax.persistence.EntityManager +import javax.sql.DataSource +import kotlin.collections.ArrayList +import kotlin.collections.List +import kotlin.collections.MutableList +import kotlin.collections.MutableSet +import kotlin.collections.Set +import kotlin.collections.drop +import kotlin.collections.emptyList +import kotlin.collections.filterNotNull +import kotlin.collections.first +import kotlin.collections.flatMap +import kotlin.collections.fold +import kotlin.collections.forEach +import kotlin.collections.groupBy +import kotlin.collections.last +import kotlin.collections.listOf +import kotlin.collections.map +import kotlin.collections.mapOf +import kotlin.collections.mutableListOf +import kotlin.collections.mutableSetOf +import kotlin.collections.plus +import kotlin.collections.plusAssign +import kotlin.collections.reversed +import kotlin.collections.setOf +import kotlin.collections.single +import kotlin.collections.toSet /** * A base node implementation that can be customised either for production (with real implementations that do real @@ -212,9 +238,11 @@ abstract class AbstractNode(val configuration: NodeConfiguration, val busyNodeLatch: ReusableLatch = ReusableLatch(), djvmBootstrapSource: ApiSource = EmptyApi, djvmCordaSource: UserSource? = null, - protected val allowHibernateToManageAppSchema: Boolean = false) : SingletonSerializeAsToken() { + protected val allowHibernateToManageAppSchema: Boolean = false, + private val allowAppSchemaUpgradeWithCheckpoints: Boolean = false) : SingletonSerializeAsToken() { protected abstract val log: Logger + @Suppress("LeakingThis") private var tokenizableServices: MutableList? = mutableListOf(platformClock, this) @@ -472,12 +500,20 @@ abstract class AbstractNode(val configuration: NodeConfiguration, } } - open fun runDatabaseMigrationScripts() { + open fun runDatabaseMigrationScripts( + updateCoreSchemas: Boolean, + updateAppSchemas: Boolean, + updateAppSchemasWithCheckpoints: Boolean + ) { check(started == null) { "Node has already been started" } Node.printBasicNodeInfo("Running database schema migration scripts ...") val props = configuration.dataSourceProperties if (props.isEmpty) throw DatabaseConfigurationException("There must be a database configured.") - database.startHikariPool(props, schemaService.internalSchemas(), metricRegistry, this.cordappLoader, configuration.baseDirectory, configuration.myLegalName, runMigrationScripts = true) + database.startHikariPool(props, metricRegistry) { dataSource, haveCheckpoints -> + SchemaMigration(dataSource, cordappLoader, configuration.baseDirectory, configuration.myLegalName) + .checkOrUpdate(schemaService.internalSchemas, updateCoreSchemas, haveCheckpoints, true) + .checkOrUpdate(schemaService.appSchemas, updateAppSchemas, !updateAppSchemasWithCheckpoints && haveCheckpoints, false) + } // Now log the vendor string as this will also cause a connection to be tested eagerly. logVendorString(database, log) if (allowHibernateToManageAppSchema) { @@ -987,7 +1023,12 @@ abstract class AbstractNode(val configuration: NodeConfiguration, protected open fun startDatabase() { val props = configuration.dataSourceProperties if (props.isEmpty) throw DatabaseConfigurationException("There must be a database configured.") - database.startHikariPool(props, schemaService.internalSchemas(), metricRegistry, this.cordappLoader, configuration.baseDirectory, configuration.myLegalName, runMigrationScripts = runMigrationScripts) + database.startHikariPool(props, metricRegistry) { dataSource, haveCheckpoints -> + SchemaMigration(dataSource, cordappLoader, configuration.baseDirectory, configuration.myLegalName) + .checkOrUpdate(schemaService.internalSchemas, runMigrationScripts, haveCheckpoints, true) + .checkOrUpdate(schemaService.appSchemas, runMigrationScripts, haveCheckpoints && !allowAppSchemaUpgradeWithCheckpoints, false) + } + // Now log the vendor string as this will also cause a connection to be tested eagerly. logVendorString(database, log) } @@ -1388,23 +1429,16 @@ fun createCordaPersistence(databaseConfig: DatabaseConfig, allowHibernateToManageAppSchema = allowHibernateToManageAppSchema) } -@Suppress("LongParameterList", "ComplexMethod", "ThrowsCount") +@Suppress("ThrowsCount") fun CordaPersistence.startHikariPool( hikariProperties: Properties, - schemas: Set, metricRegistry: MetricRegistry? = null, - cordappLoader: CordappLoader? = null, - currentDir: Path? = null, - ourName: CordaX500Name, - runMigrationScripts: Boolean = false) { + schemaMigration: (DataSource, Boolean) -> Unit) { try { val dataSource = DataSourceFactory.createDataSource(hikariProperties, metricRegistry = metricRegistry) - val schemaMigration = SchemaMigration(schemas, dataSource, cordappLoader, currentDir, ourName) - if (runMigrationScripts) { - schemaMigration.runMigration(dataSource.connection.use { DBCheckpointStorage.getCheckpointCount(it) != 0L }) - } else { - schemaMigration.checkState() - } + val haveCheckpoints = dataSource.connection.use { DBCheckpointStorage.getCheckpointCount(it) != 0L } + + schemaMigration(dataSource, haveCheckpoints) start(dataSource) } catch (ex: Exception) { when { @@ -1416,15 +1450,25 @@ fun CordaPersistence.startHikariPool( "Could not find the database driver class. Please add it to the 'drivers' folder.", NodeDatabaseErrors.MISSING_DRIVER) ex is OutstandingDatabaseChangesException -> throw (DatabaseIncompatibleException(ex.message)) - else -> + else -> { + LoggerFactory.getLogger("CordaPersistence extension").error("Could not create the DataSource", ex) throw CouldNotCreateDataSourceException( "Could not create the DataSource: ${ex.message}", NodeDatabaseErrors.FAILED_STARTUP, cause = ex) + } } } } +fun SchemaMigration.checkOrUpdate(schemas: Set, update: Boolean, haveCheckpoints: Boolean, forceThrowOnMissingMigration: Boolean): SchemaMigration { + if (update) + this.runMigration(haveCheckpoints, schemas, forceThrowOnMissingMigration) + else + this.checkState(schemas, forceThrowOnMissingMigration) + return this +} + fun clientSslOptionsCompatibleWith(nodeRpcOptions: NodeRpcOptions): ClientRpcSslOptions? { if (!nodeRpcOptions.useSsl || nodeRpcOptions.sslConfig == null) { diff --git a/node/src/main/kotlin/net/corda/node/internal/Node.kt b/node/src/main/kotlin/net/corda/node/internal/Node.kt index 884d2b57b1..cc576c40a2 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -561,11 +561,14 @@ open class Node(configuration: NodeConfiguration, return super.generateAndSaveNodeInfo() } - override fun runDatabaseMigrationScripts() { + override fun runDatabaseMigrationScripts( + updateCoreSchemas: Boolean, + updateAppSchemas: Boolean, + updateAppSchemasWithCheckpoints: Boolean) { if (allowHibernateToManageAppSchema) { initialiseSerialization() } - super.runDatabaseMigrationScripts() + super.runDatabaseMigrationScripts(updateCoreSchemas, updateAppSchemas, updateAppSchemasWithCheckpoints) } override fun start(): NodeInfo { diff --git a/node/src/main/kotlin/net/corda/node/internal/subcommands/RunMigrationScriptsCli.kt b/node/src/main/kotlin/net/corda/node/internal/subcommands/RunMigrationScriptsCli.kt index ff707e1ae5..76d86e7379 100644 --- a/node/src/main/kotlin/net/corda/node/internal/subcommands/RunMigrationScriptsCli.kt +++ b/node/src/main/kotlin/net/corda/node/internal/subcommands/RunMigrationScriptsCli.kt @@ -4,12 +4,25 @@ import net.corda.node.internal.Node import net.corda.node.internal.NodeCliCommand import net.corda.node.internal.NodeStartup import net.corda.node.internal.RunAfterNodeInitialisation +import picocli.CommandLine class RunMigrationScriptsCli(startup: NodeStartup) : NodeCliCommand("run-migration-scripts", "Run the database migration scripts and create or update schemas", startup) { + @CommandLine.Option(names = ["--core-schemas"], description = ["Manage the core/node schemas"]) + var updateCoreSchemas: Boolean = false + + @CommandLine.Option(names = ["--app-schemas"], description = ["Manage the CorDapp schemas"]) + var updateAppSchemas: Boolean = false + + @CommandLine.Option(names = ["--update-app-schema-with-checkpoints"], description = ["Allow updating app schema even if there are suspended flows"]) + var updateAppSchemaWithCheckpoints: Boolean = false + + + override fun runProgram(): Int { + require(updateAppSchemas || updateCoreSchemas) { "Nothing to do: at least one of --core-schemas or --app-schemas must be set" } return startup.initialiseAndRun(cmdLineOptions, object : RunAfterNodeInitialisation { override fun run(node: Node) { - node.runDatabaseMigrationScripts() + node.runDatabaseMigrationScripts(updateCoreSchemas, updateAppSchemas, updateAppSchemaWithCheckpoints) } }) } diff --git a/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt b/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt index d38c6371ef..a10c18aa08 100644 --- a/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt +++ b/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt @@ -62,14 +62,13 @@ class NodeSchemaService(private val extraSchemas: Set = emptySet() NodeInfoSchemaV1, NodeCoreV1) - fun internalSchemas() = requiredSchemas + extraSchemas.filter { schema -> - // when mapped schemas from the finance module are present, they are considered as internal ones - schema::class.qualifiedName == "net.corda.finance.schemas.CashSchemaV1" || - schema::class.qualifiedName == "net.corda.finance.schemas.CommercialPaperSchemaV1" || + val internalSchemas = requiredSchemas + extraSchemas.filter { schema -> schema::class.qualifiedName == "net.corda.node.services.transactions.NodeNotarySchemaV1" || schema::class.qualifiedName?.startsWith("net.corda.notary.") ?: false } + val appSchemas = extraSchemas - internalSchemas + override val schemas: Set = requiredSchemas + extraSchemas // Currently returns all schemas supported by the state, with no filtering or enrichment. diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/DbMapDeadlockTest.kt b/node/src/test/kotlin/net/corda/node/services/persistence/DbMapDeadlockTest.kt index 5befebeb6e..e063a633fc 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/DbMapDeadlockTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/DbMapDeadlockTest.kt @@ -2,11 +2,13 @@ package net.corda.node.services.persistence import net.corda.core.schemas.MappedSchema import net.corda.core.utilities.contextLogger +import net.corda.node.internal.checkOrUpdate import net.corda.node.internal.createCordaPersistence import net.corda.node.internal.startHikariPool import net.corda.node.services.schema.NodeSchemaService import net.corda.node.utilities.AppendOnlyPersistentMap import net.corda.nodeapi.internal.persistence.DatabaseConfig +import net.corda.nodeapi.internal.persistence.SchemaMigration import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.TestIdentity import net.corda.testing.internal.TestingNamedCacheFactory @@ -93,7 +95,10 @@ class DbMapDeadlockTest { val dbConfig = DatabaseConfig() val schemaService = NodeSchemaService(extraSchemas = setOf(LockDbSchemaV2)) createCordaPersistence(dbConfig, { null }, { null }, schemaService, hikariProperties, cacheFactory, null).apply { - startHikariPool(hikariProperties, schemaService.schemas, ourName = TestIdentity(ALICE_NAME, 70).name, runMigrationScripts = true) + startHikariPool(hikariProperties) { dataSource, haveCheckpoints -> + SchemaMigration(dataSource, null, null, TestIdentity(ALICE_NAME, 70).name) + .checkOrUpdate(schemaService.schemas, true, haveCheckpoints, false) + } }.use { persistence -> // First clean up any remains from previous test runs diff --git a/samples/attachment-demo/build.gradle b/samples/attachment-demo/build.gradle index e88c8fc431..70dfe3c5d8 100644 --- a/samples/attachment-demo/build.gradle +++ b/samples/attachment-demo/build.gradle @@ -90,6 +90,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar', nodeTask, } cordapp project(':samples:attachment-demo:contracts') cordapp project(':samples:attachment-demo:workflows') + runSchemaMigration = true } node { name "O=Notary Service,L=Zurich,C=CH" diff --git a/samples/bank-of-corda-demo/build.gradle b/samples/bank-of-corda-demo/build.gradle index e3ff1ad5c3..18ac7b21f6 100644 --- a/samples/bank-of-corda-demo/build.gradle +++ b/samples/bank-of-corda-demo/build.gradle @@ -48,6 +48,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar', nodeTask, nodeDefaults { cordapp project(':finance:workflows') cordapp project(':finance:contracts') + runSchemaMigration = true } node { name "O=Notary Service,L=Zurich,C=CH" diff --git a/samples/cordapp-configuration/build.gradle b/samples/cordapp-configuration/build.gradle index 9d466ed986..e0cbe8afb2 100644 --- a/samples/cordapp-configuration/build.gradle +++ b/samples/cordapp-configuration/build.gradle @@ -25,6 +25,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar', nodeTask, } rpcUsers = [['username': "default", 'password': "default", 'permissions': [ 'ALL' ]]] cordapp project(':samples:cordapp-configuration:workflows') + runSchemaMigration = true } node { name "O=Notary Service,L=Zurich,C=CH" diff --git a/samples/irs-demo/cordapp/build.gradle b/samples/irs-demo/cordapp/build.gradle index bf6c020cdf..8a0df29aef 100644 --- a/samples/irs-demo/cordapp/build.gradle +++ b/samples/irs-demo/cordapp/build.gradle @@ -60,6 +60,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar', nodeTask]) } cordapp project(':samples:irs-demo:cordapp:contracts-irs') cordapp project(':samples:irs-demo:cordapp:workflows-irs') + runSchemaMigration = true } node { name "O=Notary Service,L=Zurich,C=CH" diff --git a/samples/network-verifier/build.gradle b/samples/network-verifier/build.gradle index f7582c0069..83ff64cf24 100644 --- a/samples/network-verifier/build.gradle +++ b/samples/network-verifier/build.gradle @@ -36,6 +36,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar', nodeTask]) } cordapp project(':samples:network-verifier:contracts') cordapp project(':samples:network-verifier:workflows') + runSchemaMigration = true } node { name "O=Notary Service,L=Zurich,C=CH" diff --git a/samples/notary-demo/build.gradle b/samples/notary-demo/build.gradle index a5a7a40117..3c1280de1e 100644 --- a/samples/notary-demo/build.gradle +++ b/samples/notary-demo/build.gradle @@ -44,6 +44,7 @@ task deployNodesSingle(type: Cordform, dependsOn: ['jar', nodeTask, webTask]) { extraConfig = [h2Settings: [address: "localhost:0"]] cordapp project(':samples:notary-demo:contracts') cordapp project(':samples:notary-demo:workflows') + runSchemaMigration = true } node { name "O=Alice Corp,L=Madrid,C=ES" diff --git a/samples/simm-valuation-demo/build.gradle b/samples/simm-valuation-demo/build.gradle index f95a10716b..9736c3a998 100644 --- a/samples/simm-valuation-demo/build.gradle +++ b/samples/simm-valuation-demo/build.gradle @@ -91,6 +91,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar', nodeTask, cordapp project(':samples:simm-valuation-demo:contracts-states') cordapp project(':samples:simm-valuation-demo:flows') rpcUsers = [['username': "default", 'password': "default", 'permissions': [ 'ALL' ]]] + runSchemaMigration = true } node { name "O=Notary Service,L=Zurich,C=CH" diff --git a/samples/trader-demo/build.gradle b/samples/trader-demo/build.gradle index 5eeea06740..fa498ddcfe 100644 --- a/samples/trader-demo/build.gradle +++ b/samples/trader-demo/build.gradle @@ -81,6 +81,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar', nodeTask]) cordapp project(':finance:workflows') cordapp project(':finance:contracts') cordapp project(':samples:trader-demo:workflows-trader') + runSchemaMigration = true } node { name "O=Notary Service,L=Zurich,C=CH" diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt index b259a2aa1d..5d9751ae5d 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt @@ -127,7 +127,7 @@ open class MockServices private constructor( val cordappLoader = cordappLoaderForPackages(cordappPackages) val dataSourceProps = makeTestDataSourceProperties() val schemaService = NodeSchemaService(cordappLoader.cordappSchemas) - val database = configureDatabase(dataSourceProps, DatabaseConfig(), identityService::wellKnownPartyFromX500Name, identityService::wellKnownPartyFromAnonymous, schemaService, schemaService.internalSchemas()) + val database = configureDatabase(dataSourceProps, DatabaseConfig(), identityService::wellKnownPartyFromX500Name, identityService::wellKnownPartyFromAnonymous, schemaService, schemaService.internalSchemas) val keyManagementService = MockKeyManagementService( identityService, *arrayOf(initialIdentity.keyPair) + moreKeys @@ -170,7 +170,7 @@ open class MockServices private constructor( wellKnownPartyFromX500Name = identityService::wellKnownPartyFromX500Name, wellKnownPartyFromAnonymous = identityService::wellKnownPartyFromAnonymous, schemaService = schemaService, - internalSchemas = schemaService.internalSchemas() + internalSchemas = schemaService.internalSchemas ) val pkToIdCache = PublicKeyToOwningIdentityCacheImpl(persistence, cacheFactory) diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt index f0357e535f..4740a51ca6 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt @@ -337,6 +337,8 @@ class DriverDSLImpl( return startOutOfProcessMiniNode(config, listOfNotNull( "run-migration-scripts", + "--core-schemas", + "--app-schemas", if (hibernateForAppSchema) "--allow-hibernate-to-manage-app-schema" else null ).toTypedArray()).map { config } } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt index 5c3c3df0ce..011d65115f 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt @@ -279,7 +279,10 @@ open class InternalMockNetwork(cordappPackages: List = emptyList(), } } - open class MockNode(args: MockNodeArgs, private val mockFlowManager: MockNodeFlowManager = args.flowManager) : AbstractNode( + open class MockNode( + args: MockNodeArgs, + private val mockFlowManager: MockNodeFlowManager = args.flowManager, + allowAppSchemaUpgradeWithCheckpoints: Boolean = false) : AbstractNode( args.config, TestClock(Clock.systemUTC()), DefaultNamedCacheFactory(), @@ -287,7 +290,8 @@ open class InternalMockNetwork(cordappPackages: List = emptyList(), mockFlowManager, args.network.getServerThread(args.id), args.network.busyLatch, - allowHibernateToManageAppSchema = true + allowHibernateToManageAppSchema = true, + allowAppSchemaUpgradeWithCheckpoints = allowAppSchemaUpgradeWithCheckpoints ) { companion object { private val staticLog = contextLogger() diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt index 5a0448b5f9..8e37ceaeba 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt @@ -1,7 +1,11 @@ package net.corda.testing.internal import net.corda.core.context.AuthServiceId -import net.corda.core.contracts.* +import net.corda.core.contracts.Command +import net.corda.core.contracts.PrivacySalt +import net.corda.core.contracts.StateRef +import net.corda.core.contracts.TimeWindow +import net.corda.core.contracts.TransactionState import net.corda.core.crypto.Crypto import net.corda.core.crypto.Crypto.generateKeyPair import net.corda.core.crypto.SecureHash @@ -19,6 +23,8 @@ import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.loggerFor import net.corda.coretesting.internal.asTestContextEnv import net.corda.coretesting.internal.createTestSerializationEnv +import net.corda.coretesting.internal.stubs.CertificateStoreStubs +import net.corda.node.internal.checkOrUpdate import net.corda.node.internal.createCordaPersistence import net.corda.node.internal.security.RPCSecurityManagerImpl import net.corda.node.internal.startHikariPool @@ -32,19 +38,17 @@ import net.corda.nodeapi.internal.createDevNodeCa import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair import net.corda.nodeapi.internal.crypto.CertificateType import net.corda.nodeapi.internal.crypto.X509Utilities -import net.corda.nodeapi.internal.loadDevCaTrustStore import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig +import net.corda.nodeapi.internal.persistence.SchemaMigration import net.corda.nodeapi.internal.registerDevP2pCertificates import net.corda.serialization.internal.amqp.AMQP_ENABLED import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.SerializationEnvironmentRule import net.corda.testing.core.TestIdentity -import net.corda.coretesting.internal.stubs.CertificateStoreStubs import java.io.ByteArrayOutputStream import java.io.IOException import java.net.ServerSocket -import java.nio.file.Files import java.nio.file.Path import java.security.KeyPair import java.util.* @@ -169,7 +173,7 @@ fun configureDatabase(hikariProperties: Properties, wellKnownPartyFromX500Name: (CordaX500Name) -> Party?, wellKnownPartyFromAnonymous: (AbstractParty) -> Party?, schemaService: SchemaService = NodeSchemaService(), - internalSchemas: Set = NodeSchemaService().internalSchemas(), + internalSchemas: Set = NodeSchemaService().internalSchemas, cacheFactory: NamedCacheFactory = TestingNamedCacheFactory(), ourName: CordaX500Name = TestIdentity(ALICE_NAME, 70).name, runMigrationScripts: Boolean = true, @@ -183,7 +187,10 @@ fun configureDatabase(hikariProperties: Properties, cacheFactory, null, allowHibernateToManageAppSchema) - persistence.startHikariPool(hikariProperties, internalSchemas, ourName = ourName, runMigrationScripts = runMigrationScripts) + persistence.startHikariPool(hikariProperties) { dataSource, haveCheckpoints -> + SchemaMigration(dataSource, null, null, ourName) + .checkOrUpdate(internalSchemas, runMigrationScripts, haveCheckpoints, false) + } return persistence } From 4091fdc8b14883f000f8061bf9612e7ce5f583bd Mon Sep 17 00:00:00 2001 From: Christian Sailer Date: Thu, 18 Jun 2020 11:38:46 +0100 Subject: [PATCH 05/45] ENT-5264 Synchronise schema on the command line (#6353) * Decouple DatabaseConfig and CordaPersistence etc. * Add schema sync to schema migration + test * Add command line parameters for synchronising schema --- .../persistence/MigrationSchemaSyncTest.kt | 108 ++++++++++++++++++ .../migration/goodschema.testmigration.xml | 19 +++ .../internal/persistence/CordaPersistence.kt | 4 +- .../persistence/HibernateConfiguration.kt | 6 +- .../internal/persistence/SchemaMigration.kt | 26 +++++ .../factory/BaseSessionFactoryFactory.kt | 6 +- .../factory/CordaSessionFactoryFactory.kt | 2 - ...ibernateConfigurationFactoryLoadingTest.kt | 2 +- .../net/corda/node/internal/AbstractNode.kt | 14 ++- .../net/corda/node/internal/NodeStartup.kt | 4 +- .../subcommands/SynchroniseSchemasCli.kt | 16 +++ .../corda/node/migration/CordaMigration.kt | 9 +- 12 files changed, 198 insertions(+), 18 deletions(-) create mode 100644 node-api-tests/src/test/kotlin/net/corda/nodeapitests/internal/persistence/MigrationSchemaSyncTest.kt create mode 100644 node-api-tests/src/test/resources/migration/goodschema.testmigration.xml create mode 100644 node/src/main/kotlin/net/corda/node/internal/subcommands/SynchroniseSchemasCli.kt diff --git a/node-api-tests/src/test/kotlin/net/corda/nodeapitests/internal/persistence/MigrationSchemaSyncTest.kt b/node-api-tests/src/test/kotlin/net/corda/nodeapitests/internal/persistence/MigrationSchemaSyncTest.kt new file mode 100644 index 0000000000..a26839058d --- /dev/null +++ b/node-api-tests/src/test/kotlin/net/corda/nodeapitests/internal/persistence/MigrationSchemaSyncTest.kt @@ -0,0 +1,108 @@ +package net.corda.nodeapitests.internal.persistence + +import net.corda.core.contracts.UniqueIdentifier +import net.corda.core.schemas.MappedSchema +import net.corda.core.schemas.PersistentState +import net.corda.core.schemas.PersistentStateRef +import net.corda.node.internal.DataSourceFactory +import net.corda.node.internal.startHikariPool +import net.corda.nodeapi.internal.persistence.CordaPersistence +import net.corda.nodeapi.internal.persistence.DatabaseMigrationException +import net.corda.nodeapi.internal.persistence.HibernateSchemaChangeException +import net.corda.nodeapi.internal.persistence.SchemaMigration +import net.corda.testing.core.ALICE_NAME +import net.corda.testing.core.TestIdentity +import net.corda.testing.internal.TestingNamedCacheFactory +import net.corda.testing.node.MockServices +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.Before +import org.junit.Test +import java.util.* +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.Table +import javax.sql.DataSource + +class MigrationSchemaSyncTest{ + object TestSchemaFamily + + object GoodSchema : MappedSchema(schemaFamily = TestSchemaFamily.javaClass, version = 1, mappedTypes = listOf(State::class.java)) { + @Entity + @Table(name = "State") + class State( + @Column + var id: String + ) : PersistentState(PersistentStateRef(UniqueIdentifier().toString(), 0 )) + + override val migrationResource: String? = "goodschema.testmigration" + } + + lateinit var hikariProperties: Properties + lateinit var dataSource: DataSource + + @Before + fun setUp() { + hikariProperties = MockServices.makeTestDataSourceProperties() + dataSource = DataSourceFactory.createDataSource(hikariProperties) + } + + private fun schemaMigration() = SchemaMigration(dataSource, null, null, + TestIdentity(ALICE_NAME, 70).name) + + + @Test(timeout=300_000) + fun testSchemaScript(){ + schemaMigration().runMigration(false, setOf(GoodSchema), true) + val persistence = CordaPersistence( + false, + setOf(GoodSchema), + hikariProperties.getProperty("dataSource.url"), + TestingNamedCacheFactory() + ) + persistence.startHikariPool(hikariProperties){ _, _ -> Unit} + + persistence.transaction { + this.entityManager.persist(GoodSchema.State("id")) + } + } + + + @Test(timeout=300_000) + fun checkThatSchemaSyncFixesLiquibaseException(){ + // Schema is missing if no migration is run and hibernate not allowed to create + val persistenceBlank = CordaPersistence( + false, + setOf(GoodSchema), + hikariProperties.getProperty("dataSource.url"), + TestingNamedCacheFactory() + ) + persistenceBlank.startHikariPool(hikariProperties){ _, _ -> Unit} + assertThatThrownBy{ persistenceBlank.transaction {this.entityManager.persist(GoodSchema.State("id"))}} + .isInstanceOf(HibernateSchemaChangeException::class.java) + .hasMessageContaining("Incompatible schema") + + // create schema via hibernate - now schema gets created and we can write + val persistenceHibernate = CordaPersistence( + false, + setOf(GoodSchema), + hikariProperties.getProperty("dataSource.url"), + TestingNamedCacheFactory(), + allowHibernateToManageAppSchema = true + ) + persistenceHibernate.startHikariPool(hikariProperties){ _, _ -> Unit} + persistenceHibernate.transaction { entityManager.persist(GoodSchema.State("id_hibernate")) } + + // if we try to run schema migration now, the changelog and the schemas are out of sync + assertThatThrownBy { schemaMigration().runMigration(false, setOf(GoodSchema), true) } + .isInstanceOf(DatabaseMigrationException::class.java) + .hasMessageContaining("Table \"STATE\" already exists") + + // update the change log with schemas we know exist + schemaMigration().synchroniseSchemas(setOf(GoodSchema), true) + + // now run migration runs clean + schemaMigration().runMigration(false, setOf(GoodSchema), true) + } + + +} \ No newline at end of file diff --git a/node-api-tests/src/test/resources/migration/goodschema.testmigration.xml b/node-api-tests/src/test/resources/migration/goodschema.testmigration.xml new file mode 100644 index 0000000000..5391f357e4 --- /dev/null +++ b/node-api-tests/src/test/resources/migration/goodschema.testmigration.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/CordaPersistence.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/CordaPersistence.kt index 0535f7a8aa..4a114bed82 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/CordaPersistence.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/CordaPersistence.kt @@ -88,7 +88,7 @@ fun withoutDatabaseAccess(block: () -> T): T { val contextDatabaseOrNull: CordaPersistence? get() = _contextDatabase.get() class CordaPersistence( - databaseConfig: DatabaseConfig, + exportHibernateJMXStatistics: Boolean, schemas: Set, val jdbcUrl: String, cacheFactory: NamedCacheFactory, @@ -106,7 +106,7 @@ class CordaPersistence( val hibernateConfig: HibernateConfiguration by lazy { transaction { try { - HibernateConfiguration(schemas, databaseConfig, attributeConverters, jdbcUrl, cacheFactory, customClassLoader, allowHibernateToManageAppSchema) + HibernateConfiguration(schemas, exportHibernateJMXStatistics, attributeConverters, jdbcUrl, cacheFactory, customClassLoader, allowHibernateToManageAppSchema) } catch (e: Exception) { when (e) { is SchemaManagementException -> throw HibernateSchemaChangeException("Incompatible schema change detected. Please run schema migration scripts (node with sub-command run-migration-scripts). Reason: ${e.message}", e) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/HibernateConfiguration.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/HibernateConfiguration.kt index cfa325d5fc..d140aca312 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/HibernateConfiguration.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/HibernateConfiguration.kt @@ -19,7 +19,7 @@ import javax.persistence.AttributeConverter class HibernateConfiguration( schemas: Set, - private val databaseConfig: DatabaseConfig, + private val exportHibernateJMXStatistics: Boolean, private val attributeConverters: Collection>, jdbcUrl: String, cacheFactory: NamedCacheFactory, @@ -65,10 +65,10 @@ class HibernateConfiguration( fun sessionFactoryForSchemas(key: Set): SessionFactory = sessionFactories.get(key, ::makeSessionFactoryForSchemas)!! private fun makeSessionFactoryForSchemas(schemas: Set): SessionFactory { - val sessionFactory = sessionFactoryFactory.makeSessionFactoryForSchemas(databaseConfig, schemas, customClassLoader, attributeConverters, allowHibernateToManageAppSchema) + val sessionFactory = sessionFactoryFactory.makeSessionFactoryForSchemas(schemas, customClassLoader, attributeConverters, allowHibernateToManageAppSchema) // export Hibernate JMX statistics - if (databaseConfig.exportHibernateJMXStatistics) + if (exportHibernateJMXStatistics) initStatistics(sessionFactory) return sessionFactory diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/SchemaMigration.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/SchemaMigration.kt index cd181b0bb4..bbb9ad456a 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/SchemaMigration.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/SchemaMigration.kt @@ -93,6 +93,32 @@ open class SchemaMigration( } } + /** + * Synchronises the changelog table with the schema descriptions passed in without applying any of the changes to the database. + * This can be used when migrating a CorDapp that had its schema generated by hibernate to liquibase schema migration, or when + * updating from a version of Corda that does not use liquibase for CorDapps + * **Warning** - this will not check if the matching schema changes have been applied, it will just generate the changelog + * It must not be run on a newly installed CorDapp. + * @param schemas The set of schemas to add to the changelog + * @param forceThrowOnMissingMigration throw an exception if a mapped schema is missing its migration resource + */ + fun synchroniseSchemas(schemas: Set, forceThrowOnMissingMigration: Boolean) { + val resourcesAndSourceInfo = prepareResources(schemas, forceThrowOnMissingMigration) + + // current version of Liquibase appears to be non-threadsafe + // this is apparent when multiple in-process nodes are all running migrations simultaneously + mutex.withLock { + dataSource.connection.use { connection -> + val (runner, _, _) = prepareRunner(connection, resourcesAndSourceInfo) + try { + runner.changeLogSync(Contexts().toString()) + } catch (exp: LiquibaseException) { + throw DatabaseMigrationException(exp.message, exp) + } + } + } + } + /** Create a resource accessor that aggregates the changelogs included in the schemas into one dynamic stream. */ protected class CustomResourceAccessor(val dynamicInclude: String, val changelogList: List, classLoader: ClassLoader) : ClassLoaderResourceAccessor(classLoader) { diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/factory/BaseSessionFactoryFactory.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/factory/BaseSessionFactoryFactory.kt index e16eafa474..aa5a58148e 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/factory/BaseSessionFactoryFactory.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/factory/BaseSessionFactoryFactory.kt @@ -3,7 +3,6 @@ package net.corda.nodeapi.internal.persistence.factory import net.corda.core.schemas.MappedSchema import net.corda.core.utilities.contextLogger import net.corda.core.utilities.toHexString -import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.nodeapi.internal.persistence.HibernateConfiguration import net.corda.nodeapi.internal.persistence.TransactionIsolationLevel import org.hibernate.SessionFactory @@ -26,7 +25,7 @@ abstract class BaseSessionFactoryFactory : CordaSessionFactoryFactory { private val logger = contextLogger() } - open fun buildHibernateConfig(databaseConfig: DatabaseConfig, metadataSources: MetadataSources, allowHibernateToManageAppSchema: Boolean): Configuration { + open fun buildHibernateConfig(metadataSources: MetadataSources, allowHibernateToManageAppSchema: Boolean): Configuration { val hbm2dll: String = if (allowHibernateToManageAppSchema) { "update" @@ -82,7 +81,6 @@ abstract class BaseSessionFactoryFactory : CordaSessionFactoryFactory { } final override fun makeSessionFactoryForSchemas( - databaseConfig: DatabaseConfig, schemas: Set, customClassLoader: ClassLoader?, attributeConverters: Collection>, @@ -91,7 +89,7 @@ abstract class BaseSessionFactoryFactory : CordaSessionFactoryFactory { val serviceRegistry = BootstrapServiceRegistryBuilder().build() val metadataSources = MetadataSources(serviceRegistry) - val config = buildHibernateConfig(databaseConfig, metadataSources, allowHibernateToMananageAppSchema) + val config = buildHibernateConfig(metadataSources, allowHibernateToMananageAppSchema) schemas.forEach { schema -> schema.mappedTypes.forEach { config.addAnnotatedClass(it) } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/factory/CordaSessionFactoryFactory.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/factory/CordaSessionFactoryFactory.kt index 57b75b763a..c94034792f 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/factory/CordaSessionFactoryFactory.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/factory/CordaSessionFactoryFactory.kt @@ -1,7 +1,6 @@ package net.corda.nodeapi.internal.persistence.factory import net.corda.core.schemas.MappedSchema -import net.corda.nodeapi.internal.persistence.DatabaseConfig import org.hibernate.SessionFactory import org.hibernate.boot.Metadata import org.hibernate.boot.MetadataBuilder @@ -11,7 +10,6 @@ interface CordaSessionFactoryFactory { val databaseType: String fun canHandleDatabase(jdbcUrl: String): Boolean fun makeSessionFactoryForSchemas( - databaseConfig: DatabaseConfig, schemas: Set, customClassLoader: ClassLoader?, attributeConverters: Collection>, diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/persistence/HibernateConfigurationFactoryLoadingTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/persistence/HibernateConfigurationFactoryLoadingTest.kt index b7ad25a8d9..ff6a1b4245 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/persistence/HibernateConfigurationFactoryLoadingTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/persistence/HibernateConfigurationFactoryLoadingTest.kt @@ -14,7 +14,7 @@ class HibernateConfigurationFactoryLoadingTest { val cacheFactory = mock() HibernateConfiguration( emptySet(), - DatabaseConfig(), + false, emptyList(), jdbcUrl, cacheFactory) diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 8224ba918b..6460a851ab 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -535,6 +535,18 @@ abstract class AbstractNode(val configuration: NodeConfiguration, Node.printBasicNodeInfo("Database migration done.") } + fun runSchemaSync() { + check(started == null) { "Node has already been started" } + Node.printBasicNodeInfo("Synchronising CorDapp schemas to the changelog ...") + val hikariProperties = configuration.dataSourceProperties + if (hikariProperties.isEmpty) throw DatabaseConfigurationException("There must be a database configured.") + + val dataSource = DataSourceFactory.createDataSource(hikariProperties, metricRegistry = metricRegistry) + SchemaMigration(dataSource, cordappLoader, configuration.baseDirectory, configuration.myLegalName) + .synchroniseSchemas(schemaService.appSchemas, false) + Node.printBasicNodeInfo("CorDapp schemas synchronised") + } + open fun start(): S { check(started == null) { "Node has already been started" } @@ -1414,7 +1426,7 @@ fun createCordaPersistence(databaseConfig: DatabaseConfig, val jdbcUrl = hikariProperties.getProperty("dataSource.url", "") return CordaPersistence( - databaseConfig, + databaseConfig.exportHibernateJMXStatistics, schemaService.schemas, jdbcUrl, cacheFactory, 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 f2ae464f00..1940422fad 100644 --- a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt +++ b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt @@ -77,6 +77,7 @@ open class NodeStartupCli : CordaCliWrapper("corda", "Runs a Corda Node") { private val initialRegistrationCli by lazy { InitialRegistrationCli(startup) } private val validateConfigurationCli by lazy { ValidateConfigurationCli() } private val runMigrationScriptsCli by lazy { RunMigrationScriptsCli(startup) } + private val synchroniseAppSchemasCli by lazy { SynchroniseSchemasCli(startup) } override fun initLogging(): Boolean = this.initLogging(cmdLineOptions.baseDirectory) @@ -85,7 +86,8 @@ open class NodeStartupCli : CordaCliWrapper("corda", "Runs a Corda Node") { justGenerateRpcSslCertsCli, initialRegistrationCli, validateConfigurationCli, - runMigrationScriptsCli) + runMigrationScriptsCli, + synchroniseAppSchemasCli) override fun call(): Int { if (!validateBaseDirectory()) { diff --git a/node/src/main/kotlin/net/corda/node/internal/subcommands/SynchroniseSchemasCli.kt b/node/src/main/kotlin/net/corda/node/internal/subcommands/SynchroniseSchemasCli.kt new file mode 100644 index 0000000000..aa81d9cd5c --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/subcommands/SynchroniseSchemasCli.kt @@ -0,0 +1,16 @@ +package net.corda.node.internal.subcommands + +import net.corda.node.internal.Node +import net.corda.node.internal.NodeCliCommand +import net.corda.node.internal.NodeStartup +import net.corda.node.internal.RunAfterNodeInitialisation + +class SynchroniseSchemasCli(startup: NodeStartup) : NodeCliCommand("sync-app-schemas", "Create changelog entries for liquibase files found in CorDapps", startup) { + override fun runProgram(): Int { + return startup.initialiseAndRun(cmdLineOptions, object : RunAfterNodeInitialisation { + override fun run(node: Node) { + node.runSchemaSync() + } + }) + } +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/migration/CordaMigration.kt b/node/src/main/kotlin/net/corda/node/migration/CordaMigration.kt index 79d2910e7e..0d0832c7bd 100644 --- a/node/src/main/kotlin/net/corda/node/migration/CordaMigration.kt +++ b/node/src/main/kotlin/net/corda/node/migration/CordaMigration.kt @@ -10,9 +10,11 @@ import net.corda.core.identity.CordaX500Name import net.corda.core.schemas.MappedSchema import net.corda.node.SimpleClock import net.corda.node.services.identity.PersistentIdentityService -import net.corda.node.services.persistence.* +import net.corda.node.services.persistence.AbstractPartyToX500NameAsStringConverter +import net.corda.node.services.persistence.DBTransactionStorage +import net.corda.node.services.persistence.NodeAttachmentService +import net.corda.node.services.persistence.PublicKeyToTextConverter import net.corda.nodeapi.internal.persistence.CordaPersistence -import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.nodeapi.internal.persistence.SchemaMigration.Companion.NODE_X500_NAME import java.io.PrintWriter import java.sql.Connection @@ -74,7 +76,6 @@ abstract class CordaMigration : CustomTaskChange { cacheFactory: MigrationNamedCacheFactory, identityService: PersistentIdentityService, schema: Set): CordaPersistence { - val configDefaults = DatabaseConfig() val attributeConverters = listOf( PublicKeyToTextConverter(), AbstractPartyToX500NameAsStringConverter( @@ -83,7 +84,7 @@ abstract class CordaMigration : CustomTaskChange { ) // Liquibase handles closing the database connection when migrations are finished. If the connection is closed here, then further // migrations may fail. - return CordaPersistence(configDefaults, schema, jdbcUrl, cacheFactory, attributeConverters, closeConnection = false) + return CordaPersistence(false, schema, jdbcUrl, cacheFactory, attributeConverters, closeConnection = false) } override fun validate(database: Database?): ValidationErrors? { From 057a8d8ae9dbfa0e2ee9f62ba30fabc9f2ee12b7 Mon Sep 17 00:00:00 2001 From: Christian Sailer Date: Thu, 9 Jul 2020 15:13:20 +0100 Subject: [PATCH 06/45] NOTICK fix smoke tests and slow integration tests (#6422) * Add schema migration to smoke tests * Fix driver to work correctly for out-of-proc node with persistent database. Co-authored-by: Ross Nicoll --- .../internal/network/NetworkBootstrapper.kt | 3 +- .../registration/NodeRegistrationTest.kt | 3 +- .../testing/node/internal/DriverDSLImpl.kt | 5 ++- .../net/corda/smoketesting/NodeProcess.kt | 31 ++++++++++++++++--- 4 files changed, 32 insertions(+), 10 deletions(-) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt index 7e63bda8e8..24a08aaf01 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt @@ -80,7 +80,8 @@ constructor(private val initSerEnv: Boolean, "-jar", "corda.jar", "run-migration-scripts", - "--core-schemas" + "--core-schemas", + "--app-schemas" ) private const val LOGS_DIR_NAME = "logs" diff --git a/node/src/integration-test-slow/kotlin/net/corda/node/utilities/registration/NodeRegistrationTest.kt b/node/src/integration-test-slow/kotlin/net/corda/node/utilities/registration/NodeRegistrationTest.kt index 889a4e880e..dcf2422ef5 100644 --- a/node/src/integration-test-slow/kotlin/net/corda/node/utilities/registration/NodeRegistrationTest.kt +++ b/node/src/integration-test-slow/kotlin/net/corda/node/utilities/registration/NodeRegistrationTest.kt @@ -84,7 +84,8 @@ class NodeRegistrationTest { portAllocation = portAllocation, compatibilityZone = compatibilityZone, notarySpecs = listOf(NotarySpec(notaryName)), - notaryCustomOverrides = mapOf("devMode" to false) + notaryCustomOverrides = mapOf("devMode" to false), + allowHibernateToManageAppSchema = false ) { startNode(providedName = aliceName, customOverrides = mapOf("devMode" to false)).getOrThrow() diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt index 4740a51ca6..536d5f9c6f 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt @@ -15,7 +15,6 @@ import net.corda.core.concurrent.firstOf import net.corda.core.identity.CordaX500Name import net.corda.core.internal.PLATFORM_VERSION import net.corda.core.internal.ThreadBox -import net.corda.core.internal.concurrent.doOnComplete import net.corda.core.internal.concurrent.doOnError import net.corda.core.internal.concurrent.doneFuture import net.corda.core.internal.concurrent.flatMap @@ -260,7 +259,7 @@ class DriverDSLImpl( val config = createConfig(name, parameters, p2pAddress) val registrationFuture = if (compatibilityZone?.rootCert != null) { // We don't need the network map to be available to be able to register the node - createSchema(config, false).doOnComplete { startNodeRegistration(it, compatibilityZone.rootCert, compatibilityZone.config()) } + createSchema(config, false).flatMap { startNodeRegistration(it, compatibilityZone.rootCert, compatibilityZone.config()) } } else { doneFuture(config) } @@ -536,7 +535,7 @@ class DriverDSLImpl( ): CordaFuture> { val notaryConfig = mapOf("notary" to mapOf("validating" to spec.validating)) val parameters = NodeParameters(rpcUsers = spec.rpcUsers, verifierType = spec.verifierType, customOverrides = notaryConfig + notaryCustomOverrides, maximumHeapSize = spec.maximumHeapSize) - return createSchema(createConfig(spec.name, parameters), false).doOnComplete { config -> + return createSchema(createConfig(spec.name, parameters), false).flatMap { config -> startNodeRegistration(config, rootCert, compatibilityZone.config())}.flatMap { config -> // Node registration only gives us the node CA cert, not the identity cert. That is only created on first // startup or when the node is told to just generate its node info file. We do that here. diff --git a/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt b/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt index d39d6817ea..1daa51ae94 100644 --- a/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt +++ b/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt @@ -2,19 +2,22 @@ package net.corda.smoketesting import net.corda.client.rpc.CordaRPCClient import net.corda.client.rpc.CordaRPCConnection -import net.corda.nodeapi.internal.rpc.client.AMQPClientSerializationScheme import net.corda.core.identity.Party -import net.corda.core.internal.* +import net.corda.core.internal.createDirectories +import net.corda.core.internal.deleteRecursively +import net.corda.core.internal.div +import net.corda.core.internal.toPath +import net.corda.core.internal.writeText import net.corda.core.node.NotaryInfo import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.contextLogger import net.corda.nodeapi.internal.DevIdentityGenerator import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.network.NetworkParametersCopier +import net.corda.nodeapi.internal.rpc.client.AMQPClientSerializationScheme import net.corda.testing.common.internal.asContextEnv import net.corda.testing.common.internal.checkNotOnClasspath import net.corda.testing.common.internal.testNetworkParameters -import java.lang.IllegalStateException import java.nio.file.Path import java.nio.file.Paths import java.time.Instant @@ -32,6 +35,7 @@ class NodeProcess( companion object { const val CORDAPPS_DIR_NAME = "cordapps" private val log = contextLogger() + private const val schemaCreationTimeOutSeconds: Long = 180 } fun connect(user: User): CordaRPCConnection { @@ -103,6 +107,7 @@ class NodeProcess( (nodeDir / "node.conf").writeText(config.toText()) createNetworkParameters(NotaryInfo(notaryParty!!, true), nodeDir) + createSchema(nodeDir) val process = startNode(nodeDir) val client = CordaRPCClient(NetworkHostAndPort("localhost", config.rpcPort)) waitForNode(process, config, client) @@ -138,9 +143,25 @@ class NodeProcess( } } - private fun startNode(nodeDir: Path): Process { + class SchemaCreationTimedOutError(nodeDir: Path) : Exception("Creating node schema timed out for $nodeDir") + class SchemaCreationFailedError(nodeDir: Path) : Exception("Creating node schema failed for $nodeDir") + + + private fun createSchema(nodeDir: Path){ + val process = startNode(nodeDir, arrayOf("run-migration-scripts", "--core-schemas", "--app-schemas")) + if (!process.waitFor(schemaCreationTimeOutSeconds, SECONDS)) { + process.destroy() + throw SchemaCreationTimedOutError(nodeDir) + } + if (process.exitValue() != 0){ + throw SchemaCreationFailedError(nodeDir) + } + } + + @Suppress("SpreadOperator") + private fun startNode(nodeDir: Path, extraArgs: Array = emptyArray()): Process { val builder = ProcessBuilder() - .command(javaPath.toString(), "-Dcapsule.log=verbose", "-jar", cordaJar.toString()) + .command(javaPath.toString(), "-Dcapsule.log=verbose", "-jar", cordaJar.toString(), *extraArgs) .directory(nodeDir.toFile()) .redirectError(ProcessBuilder.Redirect.INHERIT) .redirectOutput(ProcessBuilder.Redirect.INHERIT) From adeea5c0de1d415f26b3036f5ffa4f51f595e4af Mon Sep 17 00:00:00 2001 From: Christian Sailer Date: Mon, 13 Jul 2020 10:12:57 +0100 Subject: [PATCH 07/45] NOTICK fix demobench (#6455) * Add schema migration step to node start-up * Allow to set hibernate for app schema, including error pop-up when it goes bang. --- .../corda/demobench/model/NodeController.kt | 28 ++++++++++++++++++- .../kotlin/net/corda/demobench/pty/R3Pty.kt | 5 ++++ .../net/corda/demobench/views/NodeTabView.kt | 3 ++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt index c0fee9ad29..1d3452bca1 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt @@ -1,11 +1,17 @@ package net.corda.demobench.model import javafx.application.Application.Parameters +import javafx.application.Platform import javafx.beans.binding.IntegerExpression import javafx.beans.property.SimpleBooleanProperty +import javafx.scene.control.Alert import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party -import net.corda.core.internal.* +import net.corda.core.internal.copyToDirectory +import net.corda.core.internal.createDirectories +import net.corda.core.internal.div +import net.corda.core.internal.noneOrSingle +import net.corda.core.internal.writeText import net.corda.core.node.NetworkParameters import net.corda.core.node.NotaryInfo import net.corda.core.utilities.NetworkHostAndPort @@ -52,6 +58,7 @@ class NodeController( } val djvmEnabled = SimpleBooleanProperty(djvmEnabled) + val allowHibernateToManageAppSchema = SimpleBooleanProperty(false) private val jvm by inject() private val cordappController by inject() @@ -61,6 +68,8 @@ class NodeController( private val cordaPath: Path = jvm.applicationDir.resolve("corda").resolve("corda.jar") private val command = jvm.commandFor(cordaPath).toTypedArray() + private val schemaSetupArgs = arrayOf("run-migration-scripts", "--core-schemas", "--app-schemas") + private val nodes = LinkedHashMap() private var notaryIdentity: Party? = null private var networkParametersCopier: NetworkParametersCopier? = null @@ -155,6 +164,23 @@ class NodeController( jvm.setCapsuleCacheDir(this) } (networkParametersCopier ?: makeNetworkParametersCopier(config)).install(config.nodeDir) + @Suppress("SpreadOperator") + val schemaSetupCommand = jvm.commandFor(cordaPath, *schemaSetupArgs).let { + if (allowHibernateToManageAppSchema.value) { + it + "--allow-hibernate-to-manage-app-schema" + } else { + it + } + }.toTypedArray() + if (pty.runSetupProcess(schemaSetupCommand, cordaEnv, config.nodeDir.toString()) != 0) { + Platform.runLater { + Alert( + Alert.AlertType.ERROR, + "Failed to set up database schema for node [${config.nodeConfig.myLegalName}]\n" + + "Please check logfiles!").showAndWait() + } + return false + } pty.run(command, cordaEnv, config.nodeDir.toString()) log.info("Launched node: ${config.nodeConfig.myLegalName}") return true diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/pty/R3Pty.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/pty/R3Pty.kt index 4aec5c1c79..2e20a53d10 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/pty/R3Pty.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/pty/R3Pty.kt @@ -63,6 +63,11 @@ class R3Pty(val name: CordaX500Name, settings: SettingsProvider, dimension: Dime terminal.createTerminalSession(connector).apply { start() } } + fun runSetupProcess(args: Array, envs: Map, workingDir: String?): Int { + val process = PtyProcess.exec(args, envs, workingDir) + return process.waitFor() + } + @Suppress("unused") @Throws(InterruptedException::class) fun waitFor(): Int = terminal.ttyConnector?.waitFor() ?: -1 diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTabView.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTabView.kt index 99efed7c2f..c308d1771f 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTabView.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTabView.kt @@ -155,6 +155,9 @@ class NodeTabView : Fragment() { checkbox("Deterministic Contract Verification", nodeController.djvmEnabled).apply { styleClass += "djvm" } + checkbox("Allow Hibernate to manage app schema", nodeController.allowHibernateToManageAppSchema).apply { + styleClass += "hibernate" + } } } } From 19e11619b4fac03810ef9b665c51d7fbe7c7e42f Mon Sep 17 00:00:00 2001 From: Christian Sailer Date: Thu, 23 Jul 2020 17:59:54 +0100 Subject: [PATCH 08/45] Remove unused import --- .../net/corda/node/internal/AbstractNode.kt | 25 ------------------- 1 file changed, 25 deletions(-) diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 7b0c884d50..b86fde668c 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -174,7 +174,6 @@ import org.apache.activemq.artemis.utils.ReusableLatch import org.jolokia.jvmagent.JolokiaServer import org.jolokia.jvmagent.JolokiaServerConfig import org.slf4j.Logger -import org.slf4j.LoggerFactory import rx.Scheduler import java.io.IOException import java.lang.reflect.InvocationTargetException @@ -198,30 +197,6 @@ import java.util.function.Consumer import javax.persistence.EntityManager import javax.sql.DataSource import kotlin.collections.ArrayList -import kotlin.collections.List -import kotlin.collections.MutableList -import kotlin.collections.MutableSet -import kotlin.collections.Set -import kotlin.collections.drop -import kotlin.collections.emptyList -import kotlin.collections.filterNotNull -import kotlin.collections.first -import kotlin.collections.flatMap -import kotlin.collections.fold -import kotlin.collections.forEach -import kotlin.collections.groupBy -import kotlin.collections.last -import kotlin.collections.listOf -import kotlin.collections.map -import kotlin.collections.mapOf -import kotlin.collections.mutableListOf -import kotlin.collections.mutableSetOf -import kotlin.collections.plus -import kotlin.collections.plusAssign -import kotlin.collections.reversed -import kotlin.collections.setOf -import kotlin.collections.single -import kotlin.collections.toSet /** * A base node implementation that can be customised either for production (with real implementations that do real From c4748e058823b4b71a49b075e602d05a0cd72390 Mon Sep 17 00:00:00 2001 From: Christian Sailer Date: Fri, 24 Jul 2020 10:18:45 +0100 Subject: [PATCH 09/45] Postpone notary configuration in driver DSL (matching original behaviour) to get around the service name check in registration. --- .../net/corda/testing/node/internal/DriverDSLImpl.kt | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt index 339583a8a1..2b3ead066f 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt @@ -542,8 +542,7 @@ class DriverDSLImpl( rootCert: X509Certificate, compatibilityZone: CompatibilityZoneParams ): CordaFuture> { - val notaryConfig = mapOf("notary" to mapOf("validating" to spec.validating)) - val parameters = NodeParameters(rpcUsers = spec.rpcUsers, verifierType = spec.verifierType, customOverrides = notaryConfig + notaryCustomOverrides, maximumHeapSize = spec.maximumHeapSize) + val parameters = NodeParameters(rpcUsers = spec.rpcUsers, verifierType = spec.verifierType, customOverrides = notaryCustomOverrides, maximumHeapSize = spec.maximumHeapSize) return createSchema(createConfig(spec.name, parameters), false).flatMap { config -> startNodeRegistration(config, rootCert, compatibilityZone.config())}.flatMap { config -> // Node registration only gives us the node CA cert, not the identity cert. That is only created on first @@ -551,7 +550,7 @@ class DriverDSLImpl( if (startNodesInProcess) { executorService.fork { val nodeInfo = Node(config.corda, MOCK_VERSION_INFO, initialiseSerialization = false).generateAndSaveNodeInfo() - Pair(config, NotaryInfo(nodeInfo.legalIdentities[0], spec.validating)) + Pair(config.withNotaryDefinition(spec.validating), NotaryInfo(nodeInfo.legalIdentities[0], spec.validating)) } } else { // TODO The config we use here is uses a hardocded p2p port which changes when the node is run proper @@ -563,7 +562,7 @@ class DriverDSLImpl( .get() } val nodeInfo = nodeInfoFile.readObject().verified() - Pair(config,NotaryInfo(nodeInfo.legalIdentities[0], spec.validating)) + Pair(config.withNotaryDefinition(spec.validating), NotaryInfo(nodeInfo.legalIdentities[0], spec.validating)) } } } @@ -819,6 +818,10 @@ class DriverDSLImpl( val corda: NodeConfiguration = typesafe.parseAsNodeConfiguration().value() } + private fun NodeConfig.withNotaryDefinition(validating: Boolean): NodeConfig { + return NodeConfig(this.typesafe.plus(mapOf("notary" to mapOf("validating" to validating)))) + } + companion object { private val RPC_CONNECT_POLL_INTERVAL: Duration = 100.millis internal val log = contextLogger() From 530c5eadf85149a12259bba1a6d9a0e57e93bd02 Mon Sep 17 00:00:00 2001 From: Jonathan Locke <36930160+lockathan@users.noreply.github.com> Date: Thu, 30 Jul 2020 11:27:45 +0100 Subject: [PATCH 10/45] INFRA-549: Commented out Sonatype checks during regression builds for now. (#6528) Commented out Sonatype checks during regression builds for now. --- .ci/dev/compatibility/JenkinsfileJDK11Azul | 6 ++++++ .ci/dev/regression/Jenkinsfile | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/.ci/dev/compatibility/JenkinsfileJDK11Azul b/.ci/dev/compatibility/JenkinsfileJDK11Azul index 391a2b5fc6..eb1ee42884 100644 --- a/.ci/dev/compatibility/JenkinsfileJDK11Azul +++ b/.ci/dev/compatibility/JenkinsfileJDK11Azul @@ -53,7 +53,13 @@ pipeline { } stages { + /* + * Temporarily disable Sonatype checks for regression builds + */ stage('Sonatype Check') { + when { + expression { isReleaseTag } + } steps { sh "./gradlew --no-daemon clean jar" script { diff --git a/.ci/dev/regression/Jenkinsfile b/.ci/dev/regression/Jenkinsfile index 53467aedd4..d910f9eae2 100644 --- a/.ci/dev/regression/Jenkinsfile +++ b/.ci/dev/regression/Jenkinsfile @@ -55,7 +55,13 @@ pipeline { } stages { + /* + * Temporarily disable Sonatype checks for regression builds + */ stage('Sonatype Check') { + when { + expression { isReleaseTag } + } steps { sh "./gradlew --no-daemon clean jar" script { From fd5472b0531f519a6f422d8364ce936fa1c75e93 Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Thu, 30 Jul 2020 15:39:28 +0100 Subject: [PATCH 11/45] NOTICK Remove memory leak endurance test (#6514) Remove memory leak endurance test as it spends 8 minutes testing a single failure case that's not end user visible, and ultimately manifests elsewhere in test failures (which is where this came from in the beginning). It was a good idea to confirm the change fixed the issue, but this isn't critical enough to retain. --- .../endurance/NodesStartStopSingleVmTests.kt | 30 ------------------- 1 file changed, 30 deletions(-) delete mode 100644 node/src/integration-test-slow/kotlin/net/corda/node/endurance/NodesStartStopSingleVmTests.kt diff --git a/node/src/integration-test-slow/kotlin/net/corda/node/endurance/NodesStartStopSingleVmTests.kt b/node/src/integration-test-slow/kotlin/net/corda/node/endurance/NodesStartStopSingleVmTests.kt deleted file mode 100644 index dabf8379b6..0000000000 --- a/node/src/integration-test-slow/kotlin/net/corda/node/endurance/NodesStartStopSingleVmTests.kt +++ /dev/null @@ -1,30 +0,0 @@ -package net.corda.node.endurance - -import net.corda.core.utilities.getOrThrow -import net.corda.testing.core.ALICE_NAME -import net.corda.testing.core.BOB_NAME -import net.corda.testing.driver.DriverParameters -import net.corda.testing.driver.driver -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.Parameterized - -@RunWith(Parameterized::class) -class NodesStartStopSingleVmTests(@Suppress("unused") private val iteration: Int) { - - companion object { - @JvmStatic - @Parameterized.Parameters(name = "iteration = {0}") - fun iterations(): Iterable> { - return (1..60).map { arrayOf(it) } - } - } - - @Test(timeout = 300_000) - fun nodesStartStop() { - driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { - startNode(providedName = ALICE_NAME).getOrThrow() - startNode(providedName = BOB_NAME).getOrThrow() - } - } -} \ No newline at end of file From 2a27f3ac374f567d200dda994d991cacaaf90c4f Mon Sep 17 00:00:00 2001 From: pnemeth Date: Thu, 30 Jul 2020 16:02:33 +0100 Subject: [PATCH 12/45] EG-2055 Implement network parameters hotloading (#6517) * Implement network parameters hotloading * fixed failing unit test * PR comments * PR comments * added integr tests+ renamed updater to hotloader * moved exiting logic out of hotloader * extra tests * latest PR comments * refactor * address detekt/suppress if more significant refactoring needed * extra tests * addressed PR comments * detekt * formatting --- .../node/services/network/NetworkMapTest.kt | 100 +++++++++++++- .../net/corda/node/internal/AbstractNode.kt | 29 +++- .../identity/PersistentIdentityService.kt | 12 +- .../services/network/NetworkMapUpdater.kt | 48 ++++--- .../network/NetworkParameterUpdateListener.kt | 11 ++ .../network/NetworkParametersHotloader.kt | 88 ++++++++++++ .../services/network/NotaryUpdateListener.kt | 11 ++ .../network/PersistentNetworkMapCache.kt | 8 +- .../services/network/NetworkMapUpdaterTest.kt | 4 +- .../network/NetworkParametersHotloaderTest.kt | 125 ++++++++++++++++++ .../node/internal/network/NetworkMapServer.kt | 2 +- 11 files changed, 412 insertions(+), 26 deletions(-) create mode 100644 node/src/main/kotlin/net/corda/node/services/network/NetworkParameterUpdateListener.kt create mode 100644 node/src/main/kotlin/net/corda/node/services/network/NetworkParametersHotloader.kt create mode 100644 node/src/main/kotlin/net/corda/node/services/network/NotaryUpdateListener.kt create mode 100644 node/src/test/kotlin/net/corda/node/services/network/NetworkParametersHotloaderTest.kt diff --git a/node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapTest.kt index feacf2c228..d03a1cc478 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapTest.kt @@ -1,8 +1,10 @@ package net.corda.node.services.network import net.corda.core.crypto.random63BitValue +import net.corda.core.identity.Party import net.corda.core.internal.* import net.corda.core.messaging.ParametersUpdateInfo +import net.corda.core.node.NetworkParameters import net.corda.core.node.NodeInfo import net.corda.core.serialization.serialize import net.corda.core.utilities.getOrThrow @@ -11,6 +13,7 @@ import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.network.NETWORK_PARAMS_FILE_NAME import net.corda.nodeapi.internal.network.NETWORK_PARAMS_UPDATE_FILE_NAME import net.corda.nodeapi.internal.network.SignedNetworkParameters +import net.corda.testing.common.internal.addNotary import net.corda.testing.common.internal.eventually import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.core.* @@ -74,7 +77,6 @@ class NetworkMapTest(var initFunc: (URL, NetworkMapServer) -> CompatibilityZoneP ) } - @Before fun start() { networkMapServer = NetworkMapServer(cacheTimeout, portAllocation.nextHostAndPort()) @@ -141,6 +143,102 @@ class NetworkMapTest(var initFunc: (URL, NetworkMapServer) -> CompatibilityZoneP } } + @Test(timeout = 300_000) + fun `Can hotload parameters if the notary changes`() { + internalDriver( + portAllocation = portAllocation, + compatibilityZone = compatibilityZone, + notarySpecs = emptyList() + ) { + + val notary: Party = TestIdentity.fresh("test notary").party + val oldParams = networkMapServer.networkParameters + val paramsWithNewNotary = oldParams.copy( + epoch = 3, + modifiedTime = Instant.ofEpochMilli(random63BitValue())).addNotary(notary) + + val alice = startNodeAndRunFlagDay(paramsWithNewNotary) + eventually { assertEquals(paramsWithNewNotary, alice.rpc.networkParameters) } + + } + } + + @Test(timeout = 300_000) + fun `If only the notary changes but parameters were not accepted, the node will still shut down on the flag day`() { + internalDriver( + portAllocation = portAllocation, + compatibilityZone = compatibilityZone, + notarySpecs = emptyList() + ) { + + val notary: Party = TestIdentity.fresh("test notary").party + val oldParams = networkMapServer.networkParameters + val paramsWithNewNotary = oldParams.copy( + epoch = 3, + modifiedTime = Instant.ofEpochMilli(random63BitValue())).addNotary(notary) + + val alice = startNode(providedName = ALICE_NAME, devMode = false).getOrThrow() as NodeHandleInternal + networkMapServer.scheduleParametersUpdate(paramsWithNewNotary, "Next parameters", Instant.ofEpochMilli(random63BitValue())) + // Wait for network map client to poll for the next update. + Thread.sleep(cacheTimeout.toMillis() * 2) + networkMapServer.advertiseNewParameters() + eventually { assertThatThrownBy { alice.rpc.networkParameters }.hasMessageContaining("Connection failure detected") } + + } + } + + @Test(timeout = 300_000) + fun `Can not hotload parameters if non-hotloadable parameter changes and the node will shut down`() { + internalDriver( + portAllocation = portAllocation, + compatibilityZone = compatibilityZone, + notarySpecs = emptyList() + ) { + + val oldParams = networkMapServer.networkParameters + val paramsWithUpdatedMaxMessageSize = oldParams.copy( + epoch = 3, + modifiedTime = Instant.ofEpochMilli(random63BitValue()), + maxMessageSize = oldParams.maxMessageSize + 1) + val alice = startNodeAndRunFlagDay(paramsWithUpdatedMaxMessageSize) + eventually { assertThatThrownBy { alice.rpc.networkParameters }.hasMessageContaining("Connection failure detected") } + } + } + + @Test(timeout = 300_000) + fun `Can not hotload parameters if notary and a non-hotloadable parameter changes and the node will shut down`() { + internalDriver( + portAllocation = portAllocation, + compatibilityZone = compatibilityZone, + notarySpecs = emptyList() + ) { + + val oldParams = networkMapServer.networkParameters + val notary: Party = TestIdentity.fresh("test notary").party + val paramsWithUpdatedMaxMessageSizeAndNotary = oldParams.copy( + epoch = 3, + modifiedTime = Instant.ofEpochMilli(random63BitValue()), + maxMessageSize = oldParams.maxMessageSize + 1).addNotary(notary) + val alice = startNodeAndRunFlagDay(paramsWithUpdatedMaxMessageSizeAndNotary) + eventually { assertThatThrownBy { alice.rpc.networkParameters }.hasMessageContaining("Connection failure detected") } + } + } + + private fun DriverDSLImpl.startNodeAndRunFlagDay(newParams: NetworkParameters): NodeHandleInternal { + + val alice = startNode(providedName = ALICE_NAME, devMode = false).getOrThrow() as NodeHandleInternal + val nextHash = newParams.serialize().hash + + networkMapServer.scheduleParametersUpdate(newParams, "Next parameters", Instant.ofEpochMilli(random63BitValue())) + // Wait for network map client to poll for the next update. + Thread.sleep(cacheTimeout.toMillis() * 2) + alice.rpc.acceptNewNetworkParameters(nextHash) + assertEquals(nextHash, networkMapServer.latestParametersAccepted(alice.nodeInfo.legalIdentities.first().owningKey)) + assertEquals(networkMapServer.networkParameters, alice.rpc.networkParameters) + networkMapServer.advertiseNewParameters() + return alice + } + @Test(timeout=300_000) fun `nodes process additions and removals from the network map correctly (and also download the network parameters)`() { internalDriver( diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 59b6b4fca7..296ac24da9 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -109,6 +109,8 @@ import net.corda.node.services.messaging.DeduplicationHandler import net.corda.node.services.messaging.MessagingService import net.corda.node.services.network.NetworkMapClient import net.corda.node.services.network.NetworkMapUpdater +import net.corda.node.services.network.NetworkParameterUpdateListener +import net.corda.node.services.network.NetworkParametersHotloader import net.corda.node.services.network.NodeInfoWatcher import net.corda.node.services.network.PersistentNetworkMapCache import net.corda.node.services.persistence.AbstractPartyDescriptor @@ -462,6 +464,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, } } + @Suppress("ComplexMethod") open fun start(): S { check(started == null) { "Node has already been started" } @@ -487,7 +490,8 @@ abstract class AbstractNode(val configuration: NodeConfiguration, startShell() networkMapClient?.start(trustRoot) - val (netParams, signedNetParams) = NetworkParametersReader(trustRoot, networkMapClient, configuration.baseDirectory).read() + val networkParametersReader = NetworkParametersReader(trustRoot, networkMapClient, configuration.baseDirectory) + val (netParams, signedNetParams) = networkParametersReader.read() log.info("Loaded network parameters: $netParams") check(netParams.minimumPlatformVersion <= versionInfo.platformVersion) { "Node's platform version is lower than network's required minimumPlatformVersion" @@ -508,13 +512,27 @@ abstract class AbstractNode(val configuration: NodeConfiguration, val (nodeInfo, signedNodeInfo) = nodeInfoAndSigned identityService.ourNames = nodeInfo.legalIdentities.map { it.name }.toSet() services.start(nodeInfo, netParams) + + val networkParametersHotloader = if (networkMapClient == null) { + null + } else { + NetworkParametersHotloader(networkMapClient, trustRoot, netParams, networkParametersReader, networkParametersStorage).also { + it.addNotaryUpdateListener(networkMapCache) + it.addNotaryUpdateListener(identityService) + it.addNetworkParametersChangedListeners(services) + it.addNetworkParametersChangedListeners(networkMapUpdater) + } + } + networkMapUpdater.start( trustRoot, signedNetParams.raw.hash, signedNodeInfo, netParams, keyManagementService, - configuration.networkParameterAcceptanceSettings!!) + configuration.networkParameterAcceptanceSettings!!, + networkParametersHotloader) + try { startMessagingService(rpcOps, nodeInfo, myNotaryIdentity, netParams) } catch (e: Exception) { @@ -1158,7 +1176,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, } } - inner class ServiceHubInternalImpl : SingletonSerializeAsToken(), ServiceHubInternal, ServicesForResolution by servicesForResolution { + inner class ServiceHubInternalImpl : SingletonSerializeAsToken(), ServiceHubInternal, ServicesForResolution by servicesForResolution, NetworkParameterUpdateListener { override val rpcFlows = ArrayList>>() override val stateMachineRecordedTransactionMapping = DBTransactionMappingStorage(database) override val identityService: IdentityService get() = this@AbstractNode.identityService @@ -1191,6 +1209,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, override val attachmentsClassLoaderCache: AttachmentsClassLoaderCache get() = this@AbstractNode.attachmentsClassLoaderCache + @Volatile private lateinit var _networkParameters: NetworkParameters override val networkParameters: NetworkParameters get() = _networkParameters @@ -1277,6 +1296,10 @@ abstract class AbstractNode(val configuration: NodeConfiguration, val ledgerTransaction = servicesForResolution.specialise(ltx) return verifierFactoryService.apply(ledgerTransaction) } + + override fun onNewNetworkParameters(networkParameters: NetworkParameters) { + this._networkParameters = networkParameters + } } } diff --git a/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt b/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt index ac91bdec68..d9d8906861 100644 --- a/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt +++ b/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt @@ -12,6 +12,7 @@ import net.corda.core.internal.CertRole import net.corda.core.internal.NamedCacheFactory import net.corda.core.internal.hash import net.corda.core.internal.toSet +import net.corda.core.node.NotaryInfo import net.corda.core.node.services.UnknownAnonymousPartyException import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.utilities.MAX_HASH_HEX_SIZE @@ -19,6 +20,7 @@ import net.corda.core.utilities.contextLogger import net.corda.core.utilities.debug import net.corda.node.services.api.IdentityServiceInternal import net.corda.node.services.keys.BasicHSMKeyManagementService +import net.corda.node.services.network.NotaryUpdateListener import net.corda.node.services.persistence.PublicKeyHashToExternalId import net.corda.node.services.persistence.WritablePublicKeyToOwningIdentityCache import net.corda.node.utilities.AppendOnlyPersistentMap @@ -53,7 +55,8 @@ import kotlin.streams.toList * cached for efficient lookup. */ @ThreadSafe -class PersistentIdentityService(cacheFactory: NamedCacheFactory) : SingletonSerializeAsToken(), IdentityServiceInternal { +@Suppress("TooManyFunctions") +class PersistentIdentityService(cacheFactory: NamedCacheFactory) : SingletonSerializeAsToken(), IdentityServiceInternal, NotaryUpdateListener { companion object { private val log = contextLogger() @@ -197,7 +200,8 @@ class PersistentIdentityService(cacheFactory: NamedCacheFactory) : SingletonSeri override val trustAnchor: TrustAnchor get() = _trustAnchor /** Stores notary identities obtained from the network parameters, for which we don't need to perform a database lookup. */ - private val notaryIdentityCache = HashSet() + @Volatile + private var notaryIdentityCache = HashSet() // CordaPersistence is not a c'tor parameter to work around the cyclic dependency lateinit var database: CordaPersistence @@ -453,4 +457,8 @@ class PersistentIdentityService(cacheFactory: NamedCacheFactory) : SingletonSeri keys } } + + override fun onNewNotaryList(notaries: List) { + notaryIdentityCache = HashSet(notaries.map { it.identity }) + } } \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapUpdater.kt b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapUpdater.kt index 7ff18232a2..8ecd9290ba 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapUpdater.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapUpdater.kt @@ -1,6 +1,7 @@ package net.corda.node.services.network import com.google.common.util.concurrent.MoreExecutors +import net.corda.cliutils.ExitCodes import net.corda.core.CordaRuntimeException import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SignedData @@ -61,7 +62,7 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal, private val baseDirectory: Path, private val extraNetworkMapKeys: List, private val networkParametersStorage: NetworkParametersStorage -) : AutoCloseable { +) : AutoCloseable, NetworkParameterUpdateListener { companion object { private val logger = contextLogger() private val defaultRetryInterval = 1.minutes @@ -75,12 +76,15 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal, private val fileWatcherSubscription = AtomicReference() private var autoAcceptNetworkParameters: Boolean = true private lateinit var trustRoot: X509Certificate + @Volatile private lateinit var currentParametersHash: SecureHash private lateinit var ourNodeInfo: SignedNodeInfo private lateinit var ourNodeInfoHash: SecureHash + private lateinit var networkParameters: NetworkParameters private lateinit var keyManagementService: KeyManagementService private lateinit var excludedAutoAcceptNetworkParameters: Set + private var networkParametersHotloader: NetworkParametersHotloader? = null override fun close() { fileWatcherSubscription.updateAndGet { subscription -> @@ -93,13 +97,15 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal, } MoreExecutors.shutdownAndAwaitTermination(networkMapPoller, 50, TimeUnit.SECONDS) } - + @Suppress("LongParameterList") fun start(trustRoot: X509Certificate, currentParametersHash: SecureHash, ourNodeInfo: SignedNodeInfo, networkParameters: NetworkParameters, keyManagementService: KeyManagementService, - networkParameterAcceptanceSettings: NetworkParameterAcceptanceSettings) { + networkParameterAcceptanceSettings: NetworkParameterAcceptanceSettings, + networkParametersHotloader: NetworkParametersHotloader? + ) { fileWatcherSubscription.updateAndGet { subscription -> require(subscription == null) { "Should not call this method twice" } this.trustRoot = trustRoot @@ -110,6 +116,8 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal, this.keyManagementService = keyManagementService this.autoAcceptNetworkParameters = networkParameterAcceptanceSettings.autoAcceptEnabled this.excludedAutoAcceptNetworkParameters = networkParameterAcceptanceSettings.excludedAutoAcceptableParameters + this.networkParametersHotloader = networkParametersHotloader + val autoAcceptNetworkParametersNames = autoAcceptablePropertyNames - excludedAutoAcceptNetworkParameters if (autoAcceptNetworkParameters && autoAcceptNetworkParametersNames.isNotEmpty()) { @@ -186,7 +194,7 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal, } val allHashesFromNetworkMap = (globalNetworkMap.nodeInfoHashes + additionalHashes).toSet() if (currentParametersHash != globalNetworkMap.networkParameterHash) { - exitOnParametersMismatch(globalNetworkMap) + hotloadOrExitOnParametersMismatch(globalNetworkMap) } // Calculate any nodes that are now gone and remove _only_ them from the cache // NOTE: We won't remove them until after the add/update cycle as only then will we definitely know which nodes are no longer @@ -240,22 +248,26 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal, return cacheTimeout } - private fun exitOnParametersMismatch(networkMap: NetworkMap) { + private fun hotloadOrExitOnParametersMismatch(networkMap: NetworkMap) { val updatesFile = baseDirectory / NETWORK_PARAMS_UPDATE_FILE_NAME - val acceptedHash = if (updatesFile.exists()) updatesFile.readObject().raw.hash else null - val exitCode = if (acceptedHash == networkMap.networkParameterHash) { - logger.info("Flag day occurred. Network map switched to the new network parameters: " + - "${networkMap.networkParameterHash}. Node will shutdown now and needs to be started again.") - 0 - } else { - // TODO This needs special handling (node omitted update process or didn't accept new parameters) + val newParameterHash = networkMap.networkParameterHash + val nodeAcceptedNewParameters = updatesFile.exists() && newParameterHash == updatesFile.readObject().raw.hash + + if (!nodeAcceptedNewParameters) { logger.error( """Node is using network parameters with hash $currentParametersHash but the network map is advertising ${networkMap.networkParameterHash}. To resolve this mismatch, and move to the current parameters, delete the $NETWORK_PARAMS_FILE_NAME file from the node's directory and restart. The node will shutdown now.""") - 1 + exitProcess(ExitCodes.FAILURE) } - exitProcess(exitCode) + + val hotloadSucceeded = networkParametersHotloader!!.attemptHotload(newParameterHash) + if (!hotloadSucceeded) { + logger.info("Flag day occurred. Network map switched to the new network parameters: " + + "${networkMap.networkParameterHash}. Node will shutdown now and needs to be started again.") + exitProcess(ExitCodes.SUCCESS) + } + currentParametersHash = newParameterHash } private fun handleUpdateNetworkParameters(networkMapClient: NetworkMapClient, update: ParametersUpdate) { @@ -304,6 +316,10 @@ The node will shutdown now.""") throw OutdatedNetworkParameterHashException(parametersHash, newParametersHash) } } + + override fun onNewNetworkParameters(networkParameters: NetworkParameters) { + this.networkParameters = networkParameters + } } private val memberPropertyPartition = NetworkParameters::class.declaredMemberProperties.partition { it.isAutoAcceptable() } @@ -324,8 +340,8 @@ internal fun NetworkParameters.canAutoAccept(newNetworkParameters: NetworkParame private fun KProperty1.isAutoAcceptable(): Boolean = findAnnotation() != null -private fun NetworkParameters.valueChanged(newNetworkParameters: NetworkParameters, getter: Method?): Boolean { +internal fun NetworkParameters.valueChanged(newNetworkParameters: NetworkParameters, getter: Method?): Boolean { val propertyValue = getter?.invoke(this) val newPropertyValue = getter?.invoke(newNetworkParameters) return propertyValue != newPropertyValue -} \ No newline at end of file +} diff --git a/node/src/main/kotlin/net/corda/node/services/network/NetworkParameterUpdateListener.kt b/node/src/main/kotlin/net/corda/node/services/network/NetworkParameterUpdateListener.kt new file mode 100644 index 0000000000..bce669e919 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/services/network/NetworkParameterUpdateListener.kt @@ -0,0 +1,11 @@ +package net.corda.node.services.network + +import net.corda.core.node.NetworkParameters + +/** + * When network parameters change on a flag day, onNewNetworkParameters will be invoked with the new parameters. + * Used inside {@link net.corda.node.services.network.NetworkParametersUpdater} + */ +interface NetworkParameterUpdateListener { + fun onNewNetworkParameters(networkParameters: NetworkParameters) +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/network/NetworkParametersHotloader.kt b/node/src/main/kotlin/net/corda/node/services/network/NetworkParametersHotloader.kt new file mode 100644 index 0000000000..5268e4f641 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/services/network/NetworkParametersHotloader.kt @@ -0,0 +1,88 @@ +package net.corda.node.services.network + +import net.corda.core.crypto.SecureHash +import net.corda.core.internal.NetworkParametersStorage +import net.corda.core.node.NetworkParameters +import net.corda.core.node.NotaryInfo +import net.corda.core.utilities.contextLogger +import net.corda.node.internal.NetworkParametersReader +import net.corda.nodeapi.internal.network.verifiedNetworkParametersCert +import java.security.cert.X509Certificate +import kotlin.reflect.full.declaredMemberProperties +import kotlin.reflect.jvm.javaGetter + +/** + * This class is responsible for hotloading new network parameters or shut down the node if it's not possible. + * Currently only hotloading notary changes are supported. + */ +class NetworkParametersHotloader(private val networkMapClient: NetworkMapClient, + private val trustRoot: X509Certificate, + @Volatile private var networkParameters: NetworkParameters, + private val networkParametersReader: NetworkParametersReader, + private val networkParametersStorage: NetworkParametersStorage) { + companion object { + private val logger = contextLogger() + private val alwaysHotloadable = listOf(NetworkParameters::epoch, NetworkParameters::modifiedTime) + } + + private val networkParameterUpdateListeners = mutableListOf() + private val notaryUpdateListeners = mutableListOf() + + fun addNetworkParametersChangedListeners(listener: NetworkParameterUpdateListener) { + networkParameterUpdateListeners.add(listener) + } + + fun addNotaryUpdateListener(listener: NotaryUpdateListener) { + notaryUpdateListeners.add(listener) + } + + private fun notifyListenersFor(notaries: List) = notaryUpdateListeners.forEach { it.onNewNotaryList(notaries) } + private fun notifyListenersFor(networkParameters: NetworkParameters) = networkParameterUpdateListeners.forEach { it.onNewNetworkParameters(networkParameters) } + + fun attemptHotload(newNetworkParameterHash: SecureHash): Boolean { + + val newSignedNetParams = networkMapClient.getNetworkParameters(newNetworkParameterHash) + val newNetParams = newSignedNetParams.verifiedNetworkParametersCert(trustRoot) + + if (canHotload(newNetParams)) { + logger.info("All changed parameters are hotloadable") + hotloadParameters(newNetParams) + return true + } else { + return false + } + } + + /** + * Ignoring always hotloadable properties (epoch, modifiedTime) return true if the notary is the only property that is different in the new network parameters + */ + private fun canHotload(newNetworkParameters: NetworkParameters): Boolean { + + val propertiesChanged = NetworkParameters::class.declaredMemberProperties + .minus(alwaysHotloadable) + .filter { networkParameters.valueChanged(newNetworkParameters, it.javaGetter) } + + logger.info("Updated NetworkParameters properties: $propertiesChanged") + + val noPropertiesChanged = propertiesChanged.isEmpty() + val onlyNotariesChanged = propertiesChanged == listOf(NetworkParameters::notaries) + return when { + noPropertiesChanged -> true + onlyNotariesChanged -> true + else -> false + } + } + + /** + * Update local networkParameters and currentParametersHash with new values. + * Notify all listeners for network parameter changes + */ + private fun hotloadParameters(newNetworkParameters: NetworkParameters) { + + networkParameters = newNetworkParameters + val networkParametersAndSigned = networkParametersReader.read() + networkParametersStorage.setCurrentParameters(networkParametersAndSigned.signed, trustRoot) + notifyListenersFor(newNetworkParameters) + notifyListenersFor(newNetworkParameters.notaries) + } +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/network/NotaryUpdateListener.kt b/node/src/main/kotlin/net/corda/node/services/network/NotaryUpdateListener.kt new file mode 100644 index 0000000000..6cd4570b71 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/services/network/NotaryUpdateListener.kt @@ -0,0 +1,11 @@ +package net.corda.node.services.network + +import net.corda.core.node.NotaryInfo + +/** + * When notaries inside network parameters change on a flag day, onNewNotaryList will be invoked with the new notary list. + * Used inside {@link net.corda.node.services.network.NetworkParametersUpdater} + */ +interface NotaryUpdateListener { + fun onNewNotaryList(notaries: List) +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt index f09d9aa8f6..709e415cdd 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt @@ -38,9 +38,10 @@ import javax.persistence.PersistenceException /** Database-based network map cache. */ @ThreadSafe +@Suppress("TooManyFunctions") open class PersistentNetworkMapCache(cacheFactory: NamedCacheFactory, private val database: CordaPersistence, - private val identityService: IdentityService) : NetworkMapCacheInternal, SingletonSerializeAsToken() { + private val identityService: IdentityService) : NetworkMapCacheInternal, SingletonSerializeAsToken(), NotaryUpdateListener { companion object { private val logger = contextLogger() @@ -53,6 +54,7 @@ open class PersistentNetworkMapCache(cacheFactory: NamedCacheFactory, override val nodeReady: OpenFuture = openFuture() + @Volatile private lateinit var notaries: List override val notaryIdentities: List get() = notaries.map { it.identity } @@ -386,4 +388,8 @@ open class PersistentNetworkMapCache(cacheFactory: NamedCacheFactory, for (nodeInfo in result) session.remove(nodeInfo) } } + + override fun onNewNotaryList(notaries: List) { + this.notaries = notaries + } } diff --git a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt index d2689ce039..c9dd387f36 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt @@ -76,7 +76,6 @@ class NetworkMapUpdaterTest { @Rule @JvmField val testSerialization = SerializationEnvironmentRule(true) - private val cacheExpiryMs = 1000 private val privateNetUUID = UUID.randomUUID() private val fs = Jimfs.newFileSystem(unix()) @@ -118,12 +117,13 @@ class NetworkMapUpdaterTest { networkParameters: NetworkParameters = server.networkParameters, autoAcceptNetworkParameters: Boolean = true, excludedAutoAcceptNetworkParameters: Set = emptySet()) { + updater!!.start(DEV_ROOT_CA.certificate, server.networkParameters.serialize().hash, ourNodeInfo, networkParameters, MockKeyManagementService(makeTestIdentityService(), ourKeyPair), - NetworkParameterAcceptanceSettings(autoAcceptNetworkParameters, excludedAutoAcceptNetworkParameters)) + NetworkParameterAcceptanceSettings(autoAcceptNetworkParameters, excludedAutoAcceptNetworkParameters), null) } @Test(timeout=300_000) diff --git a/node/src/test/kotlin/net/corda/node/services/network/NetworkParametersHotloaderTest.kt b/node/src/test/kotlin/net/corda/node/services/network/NetworkParametersHotloaderTest.kt new file mode 100644 index 0000000000..1fcd40cf42 --- /dev/null +++ b/node/src/test/kotlin/net/corda/node/services/network/NetworkParametersHotloaderTest.kt @@ -0,0 +1,125 @@ +package net.corda.node.services.network + +import com.nhaarman.mockito_kotlin.any +import com.nhaarman.mockito_kotlin.never +import com.nhaarman.mockito_kotlin.verify +import net.corda.core.identity.Party +import net.corda.core.internal.NetworkParametersStorage +import net.corda.core.node.NetworkParameters +import net.corda.core.node.NotaryInfo +import net.corda.core.serialization.serialize +import net.corda.coretesting.internal.DEV_ROOT_CA +import net.corda.node.internal.NetworkParametersReader +import net.corda.nodeapi.internal.createDevNetworkMapCa +import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair +import net.corda.testing.common.internal.addNotary +import net.corda.testing.common.internal.testNetworkParameters +import net.corda.testing.core.SerializationEnvironmentRule +import net.corda.testing.core.TestIdentity +import org.junit.Assert +import org.junit.Rule +import org.junit.Test +import org.mockito.Mockito + +class NetworkParametersHotloaderTest { + @Rule + @JvmField + val testSerialization = SerializationEnvironmentRule(true) + private val networkMapCertAndKeyPair: CertificateAndKeyPair = createDevNetworkMapCa() + private val trustRoot = DEV_ROOT_CA.certificate + + private val originalNetworkParameters = testNetworkParameters() + private val notary: Party = TestIdentity.fresh("test notary").party + private val networkParametersWithNotary = originalNetworkParameters.addNotary(notary) + private val networkParametersStorage = Mockito.mock(NetworkParametersStorage::class.java) + + @Test(timeout = 300_000) + fun `can hotload if notary changes`() { + `can hotload`(networkParametersWithNotary) + } + + @Test(timeout = 300_000) + fun `can not hotload if notary changes but another non-hotloadable property also changes`() { + + val newnetParamsWithNewNotaryAndMaxMsgSize = networkParametersWithNotary.copy(maxMessageSize = networkParametersWithNotary.maxMessageSize + 1) + `can not hotload`(newnetParamsWithNewNotaryAndMaxMsgSize) + } + + @Test(timeout = 300_000) + fun `can hotload if only always hotloadable properties change`() { + + val newParametersWithAlwaysHotloadableProperties = originalNetworkParameters.copy(epoch = originalNetworkParameters.epoch + 1, modifiedTime = originalNetworkParameters.modifiedTime.plusSeconds(60)) + `can hotload`(newParametersWithAlwaysHotloadableProperties) + } + + @Test(timeout = 300_000) + fun `can not hotload if maxMessageSize changes`() { + + val parametersWithNewMaxMessageSize = originalNetworkParameters.copy(maxMessageSize = originalNetworkParameters.maxMessageSize + 1) + `can not hotload`(parametersWithNewMaxMessageSize) + } + + @Test(timeout = 300_000) + fun `can not hotload if maxTransactionSize changes`() { + + val parametersWithNewMaxTransactionSize = originalNetworkParameters.copy(maxTransactionSize = originalNetworkParameters.maxMessageSize + 1) + `can not hotload`(parametersWithNewMaxTransactionSize) + } + + @Test(timeout = 300_000) + fun `can not hotload if minimumPlatformVersion changes`() { + + val parametersWithNewMinimumPlatformVersion = originalNetworkParameters.copy(minimumPlatformVersion = originalNetworkParameters.minimumPlatformVersion + 1) + `can not hotload`(parametersWithNewMinimumPlatformVersion) + } + + private fun `can hotload`(newNetworkParameters: NetworkParameters) { + val notaryUpdateListener = Mockito.spy(object : NotaryUpdateListener { + override fun onNewNotaryList(notaries: List) { + } + }) + + val networkParametersChangedListener = Mockito.spy(object : NetworkParameterUpdateListener { + override fun onNewNetworkParameters(networkParameters: NetworkParameters) { + } + }) + val networkParametersHotloader = createHotloaderWithMockedServices(newNetworkParameters).also { + it.addNotaryUpdateListener(notaryUpdateListener) + it.addNetworkParametersChangedListeners(networkParametersChangedListener) + } + + Assert.assertTrue(networkParametersHotloader.attemptHotload(newNetworkParameters.serialize().hash)) + verify(notaryUpdateListener).onNewNotaryList(newNetworkParameters.notaries) + verify(networkParametersChangedListener).onNewNetworkParameters(newNetworkParameters) + } + + private fun `can not hotload`(newNetworkParameters: NetworkParameters) { + val notaryUpdateListener = Mockito.spy(object : NotaryUpdateListener { + override fun onNewNotaryList(notaries: List) { + } + }) + + val networkParametersChangedListener = Mockito.spy(object : NetworkParameterUpdateListener { + override fun onNewNetworkParameters(networkParameters: NetworkParameters) { + } + }) + val networkParametersHotloader = createHotloaderWithMockedServices(newNetworkParameters).also { + it.addNotaryUpdateListener(notaryUpdateListener) + it.addNetworkParametersChangedListeners(networkParametersChangedListener) + } + Assert.assertFalse(networkParametersHotloader.attemptHotload(newNetworkParameters.serialize().hash)) + verify(notaryUpdateListener, never()).onNewNotaryList(any()); + verify(networkParametersChangedListener, never()).onNewNetworkParameters(any()); + } + + private fun createHotloaderWithMockedServices(newNetworkParameters: NetworkParameters): NetworkParametersHotloader { + val signedNetworkParameters = networkMapCertAndKeyPair.sign(newNetworkParameters) + val networkMapClient = Mockito.mock(NetworkMapClient::class.java) + Mockito.`when`(networkMapClient.getNetworkParameters(newNetworkParameters.serialize().hash)).thenReturn(signedNetworkParameters) + val networkParametersReader = Mockito.mock(NetworkParametersReader::class.java) + Mockito.`when`(networkParametersReader.read()) + .thenReturn(NetworkParametersReader.NetworkParametersAndSigned(signedNetworkParameters, trustRoot)) + return NetworkParametersHotloader(networkMapClient, trustRoot, originalNetworkParameters, networkParametersReader, networkParametersStorage) + } +} + diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/network/NetworkMapServer.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/network/NetworkMapServer.kt index 0aa00f832c..c29f21ff84 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/network/NetworkMapServer.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/network/NetworkMapServer.kt @@ -115,7 +115,7 @@ class NetworkMapServer(private val pollInterval: Duration, // Mapping from the UUID of the network (null for global one) to hashes of the nodes in network private val networkMaps = mutableMapOf>() val latestAcceptedParametersMap = mutableMapOf() - private val signedNetParams by lazy { networkMapCertAndKeyPair.sign(networkParameters) } + private val signedNetParams get() = networkMapCertAndKeyPair.sign(networkParameters) @POST @Path("publish") From 87faf35ecb34026582a6b8807ae578a9e5e4b703 Mon Sep 17 00:00:00 2001 From: Paul Hatcher <64464377+PaulHatcherR3@users.noreply.github.com> Date: Thu, 30 Jul 2020 17:08:00 +0100 Subject: [PATCH 13/45] CORDA-3929 : quasar 0.7.12_r3 -> quasar 0.7.13_r3 (#6522) --- constants.properties | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/constants.properties b/constants.properties index e6fa88e69d..af2f232ddd 100644 --- a/constants.properties +++ b/constants.properties @@ -14,8 +14,7 @@ java8MinUpdateVersion=171 platformVersion=8 guavaVersion=28.0-jre # Quasar version to use with Java 8: -quasarVersion=0.7.12_r3 -quasarClassifier=jdk8 +quasarVersion=0.7.13_r3 # Quasar version to use with Java 11: quasarVersion11=0.8.0_r3 jdkClassifier11=jdk11 From 68feb1c35fa37379f492803fb3bf047321883d4d Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Fri, 31 Jul 2020 08:32:20 +0100 Subject: [PATCH 14/45] CORDA-3932 Correct race condition in FlowVersioningTest (#6536) Correct race condition in FlowVersioningTest where the last message is read (and the session close can be triggered) before one side has finished reading metadata from the session. --- .../statemachine/FlowVersioningTest.kt | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/node/src/integration-test/kotlin/net/corda/node/services/statemachine/FlowVersioningTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/statemachine/FlowVersioningTest.kt index e2fc51282b..a6b04d6441 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/statemachine/FlowVersioningTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/statemachine/FlowVersioningTest.kt @@ -33,13 +33,19 @@ class FlowVersioningTest : NodeBasedTest() { private class PretendInitiatingCoreFlow(val initiatedParty: Party) : FlowLogic>() { @Suspendable override fun call(): Pair { - // Execute receive() outside of the Pair constructor to avoid Kotlin/Quasar instrumentation bug. val session = initiateFlow(initiatedParty) - val alicePlatformVersionAccordingToBob = session.receive().unwrap { it } - return Pair( - alicePlatformVersionAccordingToBob, - session.getCounterpartyFlowInfo().flowVersion - ) + return try { + // Get counterparty flow info before we receive Alice's data, to ensure the flow is still open + val bobPlatformVersionAccordingToAlice = session.getCounterpartyFlowInfo().flowVersion + // Execute receive() outside of the Pair constructor to avoid Kotlin/Quasar instrumentation bug. + val alicePlatformVersionAccordingToBob = session.receive().unwrap { it } + Pair( + alicePlatformVersionAccordingToBob, + bobPlatformVersionAccordingToAlice + ) + } finally { + session.close() + } } } From c498c5bf7c3e1e668d7e0472ab2adc43e4029a8e Mon Sep 17 00:00:00 2001 From: Viktor Kolomeyko Date: Fri, 31 Jul 2020 09:26:32 +0100 Subject: [PATCH 15/45] CORDA-3871: New integration test for handshake timeout in AMQPClient (#6519) * CORDA-3871: Import external code Compiles, but does not work for various reasons * CORDA-3871: More improvements to imported code Currently fails due to keystores not being found * CORDA-3871: Initialise keystores for the server Currently fails due to keystores for client not being found * CORDA-3871: Configure certificates to client The program started to run * CORDA-3871: Improve debug output * CORDA-3871: Few more minor changes * CORDA-3871: Add AMQClient test Currently fails due to `localCert` not being set * CORDA-3871: Configure server to demand client to present its certificate * CORDA-3871: Changes to the test to make it pass ACK status is not delivered as server is not talking AMQP * CORDA-3871: Add delayed handshake scenario * CORDA-3871: Tidy-up imported classes * CORDA-3871: Hide thread creation inside `ServerThread` * CORDA-3871: Test description * CORDA-3871: Detekt baseline update * CORDA-3871: Trigger repeated execution of new tests To make sure they are not flaky * CORDA-3871: Improve robustness of the newly introduced tests * CORDA-3871: Improve robustness of the newly introduced tests * CORDA-3871: New tests proven to be stable - reduce number of iterations to 1 * CORDA-3871: Adding Alex Karnezis to the list of contributors --- CONTRIBUTORS.md | 1 + detekt-baseline.xml | 72 +--- .../internal/protonwrapper/netty/SSLHelper.kt | 19 +- .../net/corda/node/amqp/NioSslClient.java | 216 ++++++++++++ .../java/net/corda/node/amqp/NioSslPeer.java | 329 ++++++++++++++++++ .../net/corda/node/amqp/NioSslServer.java | 263 ++++++++++++++ .../net/corda/node/amqp/ServerThread.java | 69 ++++ .../node/amqp/AMQPClientSslErrorsTest.kt | 208 +++++++++++ 8 files changed, 1098 insertions(+), 79 deletions(-) create mode 100644 node/src/integration-test/java/net/corda/node/amqp/NioSslClient.java create mode 100644 node/src/integration-test/java/net/corda/node/amqp/NioSslPeer.java create mode 100644 node/src/integration-test/java/net/corda/node/amqp/NioSslServer.java create mode 100644 node/src/integration-test/java/net/corda/node/amqp/ServerThread.java create mode 100644 node/src/integration-test/kotlin/net/corda/node/amqp/AMQPClientSslErrorsTest.kt diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 761ec63330..5a8f885e93 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -13,6 +13,7 @@ see changes to this list. * agoldvarg * Ajitha Thayaharan (BCS Technology International) * Alberto Arri (R3) +* Alex Karnezis * amiracam * Amol Pednekar * Andras Slemmer (R3) diff --git a/detekt-baseline.xml b/detekt-baseline.xml index 974e679f57..401dfbe681 100644 --- a/detekt-baseline.xml +++ b/detekt-baseline.xml @@ -9,23 +9,11 @@ ClassNaming:BuyerFlow.kt$BuyerFlow$STARTING_BUY : Step ClassNaming:CompositeMemberCompositeSchemaToClassCarpenterTests.kt$I_ ClassNaming:CordaServiceTest.kt$CordaServiceTest.DummyServiceFlow.Companion$TEST_STEP : Step - ClassNaming:CustomVaultQuery.kt$TopupIssuerFlow.TopupIssuer.Companion$AWAITING_REQUEST : Step - ClassNaming:CustomVaultQuery.kt$TopupIssuerFlow.TopupIssuer.Companion$SENDING_TOP_UP_ISSUE_REQUEST : Step ClassNaming:DeserializeNeedingCarpentryTests.kt$DeserializeNeedingCarpentryTests$outer ClassNaming:FlowCheckpointCordapp.kt$SendMessageFlow.Companion$FINALISING_TRANSACTION : Step ClassNaming:FlowCheckpointCordapp.kt$SendMessageFlow.Companion$GENERATING_TRANSACTION : Step ClassNaming:FlowCheckpointCordapp.kt$SendMessageFlow.Companion$SIGNING_TRANSACTION : Step ClassNaming:FlowCheckpointCordapp.kt$SendMessageFlow.Companion$VERIFYING_TRANSACTION : Step - ClassNaming:FlowCookbook.kt$InitiatorFlow.Companion$EXTRACTING_VAULT_STATES : Step - ClassNaming:FlowCookbook.kt$InitiatorFlow.Companion$ID_OTHER_NODES : Step - ClassNaming:FlowCookbook.kt$InitiatorFlow.Companion$OTHER_TX_COMPONENTS : Step - ClassNaming:FlowCookbook.kt$InitiatorFlow.Companion$SENDING_AND_RECEIVING_DATA : Step - ClassNaming:FlowCookbook.kt$InitiatorFlow.Companion$SIGS_GATHERING : Step - ClassNaming:FlowCookbook.kt$InitiatorFlow.Companion$TX_BUILDING : Step - ClassNaming:FlowCookbook.kt$InitiatorFlow.Companion$TX_SIGNING : Step - ClassNaming:FlowCookbook.kt$InitiatorFlow.Companion$TX_VERIFICATION : Step - ClassNaming:FlowCookbook.kt$InitiatorFlow.Companion$VERIFYING_SIGS : Step - ClassNaming:FlowCookbook.kt$ResponderFlow.Companion$RECEIVING_AND_SENDING_DATA : Step ClassNaming:FlowFrameworkTests.kt$ExceptionFlow$START_STEP : Step ClassNaming:FlowFrameworkTests.kt$InitiatedReceiveFlow$RECEIVED_STEP : Step ClassNaming:FlowFrameworkTests.kt$InitiatedReceiveFlow$START_STEP : Step @@ -178,7 +166,6 @@ ComplexMethod:RPCServer.kt$RPCServer$private fun clientArtemisMessageHandler(artemisMessage: ClientMessage) ComplexMethod:ReconnectingCordaRPCOps.kt$ReconnectingCordaRPCOps.ReconnectingRPCConnection$ private tailrec fun establishConnectionWithRetry( retryInterval: Duration, roundRobinIndex: Int = 0, retries: Int = -1 ): CordaRPCConnection? ComplexMethod:RemoteTypeCarpenter.kt$SchemaBuildingRemoteTypeCarpenter$override fun carpent(typeInformation: RemoteTypeInformation): Type - ComplexMethod:RpcReconnectTests.kt$RpcReconnectTests$ @Test(timeout=300_000) fun `test that the RPC client is able to reconnect and proceed after node failure, restart, or connection reset`() ComplexMethod:SchemaMigration.kt$SchemaMigration$ private fun migrateOlderDatabaseToUseLiquibase(existingCheckpoints: Boolean): Boolean ComplexMethod:SchemaMigration.kt$SchemaMigration$private fun doRunMigration( run: Boolean, check: Boolean, existingCheckpoints: Boolean? = null ) ComplexMethod:SendTransactionFlow.kt$DataVendingFlow$@Suspendable override fun call(): Void? @@ -310,7 +297,6 @@ ForbiddenComment:CordappProviderImplTests.kt$CordappProviderImplTests.Companion$// TODO: Cordapp name should differ from the JAR name ForbiddenComment:CoreFlowHandlers.kt$NotaryChangeHandler$// TODO: Right now all nodes will automatically approve the notary change. We need to figure out if stricter controls are necessary. ForbiddenComment:CrossCashTest.kt$CrossCashState$// TODO: Alternative: We may possibly reduce the complexity of the search even further using some form of - ForbiddenComment:Crypto.kt$Crypto$// TODO: Check if non-ECC keys satisfy params (i.e. approved/valid RSA modulus size). ForbiddenComment:Crypto.kt$Crypto$// TODO: We currently use SHA256(seed) when retrying, but BIP32 just skips a counter (i) that results to an invalid key. ForbiddenComment:Crypto.kt$Crypto$// TODO: change the val name to a more descriptive one as it's now confusing and looks like a Key type. ForbiddenComment:Crypto.kt$Crypto$// TODO: change val name to SPHINCS256_SHA512. This will break backwards compatibility. @@ -435,7 +421,6 @@ ForbiddenComment:RatesFixFlow.kt$RatesFixFlow.FixQueryFlow$// TODO: add deadline to receive ForbiddenComment:ResolveTransactionsFlow.kt$ResolveTransactionsFlow$// TODO: This could be done in parallel with other fetches for extra speed. ForbiddenComment:ResolveTransactionsFlowTest.kt$ResolveTransactionsFlowTest$// TODO: this operation should not require an explicit transaction - ForbiddenComment:RestrictedEntityManager.kt$RestrictedEntityManager$// TODO: Figure out which other methods on EntityManager need to be blocked? ForbiddenComment:ScheduledActivityObserver.kt$ScheduledActivityObserver.Companion$// TODO: Beware we are calling dynamically loaded contract code inside here. ForbiddenComment:ScheduledFlowIntegrationTests.kt$ScheduledFlowIntegrationTests$// TODO: the queries below are not atomic so we need to allow enough time for the scheduler to finish. Would be better to query scheduler. ForbiddenComment:SendTransactionFlow.kt$DataVendingFlow$// Security TODO: Check for abnormally large or malformed data requests @@ -622,7 +607,6 @@ FunctionNaming:VersionExtractorTest.kt$VersionExtractorTest$@Test(timeout=300_000) fun version_header_extraction_present() LargeClass:AbstractNode.kt$AbstractNode<S> : SingletonSerializeAsToken LargeClass:SingleThreadedStateMachineManager.kt$SingleThreadedStateMachineManager : StateMachineManagerStateMachineManagerInternal - LongMethod:FlowCookbook.kt$InitiatorFlow$@Suppress("RemoveExplicitTypeArguments") @Suspendable override fun call() LongMethod:HibernateQueryCriteriaParser.kt$HibernateQueryCriteriaParser$override fun parseCriteria(criteria: CommonQueryCriteria): Collection<Predicate> LongParameterList:AMQPSerializer.kt$AMQPSerializer$(obj: Any, data: Data, type: Type, output: SerializationOutput, context: SerializationContext, debugIndent: Int = 0) LongParameterList:AbstractCashSelection.kt$AbstractCashSelection$(connection: Connection, amount: Amount<Currency>, lockId: UUID, notary: Party?, onlyFromIssuerParties: Set<AbstractParty>, withIssuerRefs: Set<OpaqueBytes>, withResultSet: (ResultSet) -> Boolean) @@ -634,7 +618,6 @@ LongParameterList:ArtemisRpcBroker.kt$ArtemisRpcBroker.Companion$(configuration: MutualSslConfiguration, address: NetworkHostAndPort, adminAddress: NetworkHostAndPort, securityManager: RPCSecurityManager, maxMessageSize: Int, journalBufferTimeout: Int?, jmxEnabled: Boolean, baseDirectory: Path, shouldStartLocalShell: Boolean) LongParameterList:ArtemisRpcBroker.kt$ArtemisRpcBroker.Companion$(configuration: MutualSslConfiguration, address: NetworkHostAndPort, adminAddress: NetworkHostAndPort, sslOptions: BrokerRpcSslOptions, securityManager: RPCSecurityManager, maxMessageSize: Int, journalBufferTimeout: Int?, jmxEnabled: Boolean, baseDirectory: Path, shouldStartLocalShell: Boolean) LongParameterList:ArtemisRpcTests.kt$ArtemisRpcTests$(nodeSSlconfig: MutualSslConfiguration, brokerSslOptions: BrokerRpcSslOptions?, useSslForBroker: Boolean, clientSslOptions: ClientRpcSslOptions?, address: NetworkHostAndPort = ports.nextHostAndPort(), adminAddress: NetworkHostAndPort = ports.nextHostAndPort(), baseDirectory: Path = tempFolder.root.toPath() ) - LongParameterList:AttachmentsClassLoader.kt$AttachmentsClassLoaderBuilder$(attachments: List<Attachment>, params: NetworkParameters, txId: SecureHash, isAttachmentTrusted: (Attachment) -> Boolean, parent: ClassLoader = ClassLoader.getSystemClassLoader(), block: (ClassLoader) -> T) LongParameterList:BFTSmart.kt$BFTSmart.Replica$( states: List<StateRef>, txId: SecureHash, callerName: CordaX500Name, requestSignature: NotarisationRequestSignature, timeWindow: TimeWindow?, references: List<StateRef> = emptyList() ) LongParameterList:BusinessCalendar.kt$BusinessCalendar.Companion$(startDate: LocalDate, period: Frequency, calendar: BusinessCalendar = EMPTY, dateRollConvention: DateRollConvention = DateRollConvention.Following, noOfAdditionalPeriods: Int = Integer.MAX_VALUE, endDate: LocalDate? = null, periodOffset: Int? = null) LongParameterList:Cash.kt$Cash$(inputs: List<State>, outputs: List<State>, tx: LedgerTransaction, issueCommand: CommandWithParties<Commands.Issue>, currency: Currency, issuer: PartyAndReference) @@ -732,7 +715,6 @@ LongParameterList:X509Utilities.kt$X509Utilities$(certificateType: CertificateType, issuer: X500Principal, issuerPublicKey: PublicKey, issuerSigner: ContentSigner, subject: X500Principal, subjectPublicKey: PublicKey, validityWindow: Pair<Date, Date>, nameConstraints: NameConstraints? = null, crlDistPoint: String? = null, crlIssuer: X500Name? = null) LongParameterList:X509Utilities.kt$X509Utilities$(certificateType: CertificateType, issuer: X500Principal, issuerPublicKey: PublicKey, subject: X500Principal, subjectPublicKey: PublicKey, validityWindow: Pair<Date, Date>, nameConstraints: NameConstraints? = null, crlDistPoint: String? = null, crlIssuer: X500Name? = null) LongParameterList:X509Utilities.kt$X509Utilities$(certificateType: CertificateType, issuerCertificate: X509Certificate, issuerKeyPair: KeyPair, subject: X500Principal, subjectPublicKey: PublicKey, validityWindow: Pair<Duration, Duration> = DEFAULT_VALIDITY_WINDOW, nameConstraints: NameConstraints? = null, crlDistPoint: String? = null, crlIssuer: X500Name? = null) - LongParameterList:internalAccessTestHelpers.kt$( inputs: List<StateAndRef<ContractState>>, outputs: List<TransactionState<ContractState>>, commands: List<CommandWithParties<CommandData>>, attachments: List<Attachment>, id: SecureHash, notary: Party?, timeWindow: TimeWindow?, privacySalt: PrivacySalt, networkParameters: NetworkParameters, references: List<StateAndRef<ContractState>>, componentGroups: List<ComponentGroup>? = null, serializedInputs: List<SerializedStateAndRef>? = null, serializedReferences: List<SerializedStateAndRef>? = null, isAttachmentTrusted: (Attachment) -> Boolean ) MagicNumber:AMQPClientSerializationScheme.kt$AMQPClientSerializationScheme.Companion$128 MagicNumber:AMQPSerializationScheme.kt$AbstractAMQPSerializationScheme$128 MagicNumber:AMQPServer.kt$AMQPServer$100 @@ -753,7 +735,6 @@ MagicNumber:AttachmentDemo.kt$10009 MagicNumber:AttachmentDemo.kt$10010 MagicNumber:AttachmentTrustTable.kt$AttachmentTrustTable$3 - MagicNumber:AttachmentsClassLoader.kt$AttachmentsClassLoader$4 MagicNumber:AzureSmbVolume.kt$AzureSmbVolume$5000 MagicNumber:BFTSmart.kt$BFTSmart.Client$100 MagicNumber:BFTSmart.kt$BFTSmart.Replica.<no name provided>$20000 @@ -775,12 +756,6 @@ MagicNumber:CashViewer.kt$CashViewer.StateRowGraphic$16 MagicNumber:CashViewer.kt$CashViewer.StateRowGraphic$30.0 MagicNumber:ClassCarpenter.kt$ClassCarpenterImpl$3 - MagicNumber:ClientRpcExample.kt$ClientRpcExample$3 - MagicNumber:ClientRpcTutorial.kt$0.7 - MagicNumber:ClientRpcTutorial.kt$0.8 - MagicNumber:ClientRpcTutorial.kt$1000 - MagicNumber:ClientRpcTutorial.kt$10000 - MagicNumber:ClientRpcTutorial.kt$2000 MagicNumber:CommercialPaperIssueFlow.kt$CommercialPaperIssueFlow$10 MagicNumber:CommercialPaperIssueFlow.kt$CommercialPaperIssueFlow$30 MagicNumber:CompositeSignature.kt$CompositeSignature$1024 @@ -846,11 +821,6 @@ MagicNumber:ExchangeRateModel.kt$1.18 MagicNumber:ExchangeRateModel.kt$1.31 MagicNumber:FixingFlow.kt$FixingFlow.Fixer.<no name provided>$30 - MagicNumber:FlowCookbook.kt$InitiatorFlow$30 - MagicNumber:FlowCookbook.kt$InitiatorFlow$45 - MagicNumber:FlowCookbook.kt$InitiatorFlow$777 - MagicNumber:FlowCookbook.kt$ResponderFlow$99 - MagicNumber:FlowCookbook.kt$ResponderFlow.<no name provided>$777 MagicNumber:FlowLogic.kt$FlowLogic$300 MagicNumber:FlowLogic.kt$FlowLogic.Companion$5 MagicNumber:FlowMonitor.kt$FlowMonitor$1000 @@ -864,7 +834,6 @@ MagicNumber:HTTPNetworkRegistrationService.kt$HTTPNetworkRegistrationService$10 MagicNumber:HttpUtils.kt$HttpUtils$5 MagicNumber:HttpUtils.kt$HttpUtils$60 - MagicNumber:IOUFlowResponder.kt$IOUFlowResponder.<no name provided>$100 MagicNumber:IRS.kt$RatePaymentEvent$360.0 MagicNumber:IRS.kt$RatePaymentEvent$4 MagicNumber:IRS.kt$RatePaymentEvent$8 @@ -1026,7 +995,6 @@ MagicNumber:NodeWebServer.kt$NodeWebServer$100 MagicNumber:NodeWebServer.kt$NodeWebServer$32768 MagicNumber:NodeWebServer.kt$NodeWebServer$40 - MagicNumber:NonValidatingNotaryFlow.kt$NonValidatingNotaryFlow$4 MagicNumber:Notarise.kt$10 MagicNumber:Notarise.kt$10003 MagicNumber:NullKeys.kt$NullKeys$32 @@ -1143,7 +1111,6 @@ MagicNumber:StandaloneShell.kt$StandaloneShell$7 MagicNumber:StateRevisionFlow.kt$StateRevisionFlow.Requester$30 MagicNumber:Structures.kt$PrivacySalt$32 - MagicNumber:TargetVersionDependentRules.kt$StateContractValidationEnforcementRule$4 MagicNumber:TestNodeInfoBuilder.kt$TestNodeInfoBuilder$1234 MagicNumber:TestUtils.kt$10000 MagicNumber:TestUtils.kt$30000 @@ -1154,7 +1121,6 @@ MagicNumber:TraderDemoClientApi.kt$TraderDemoClientApi$3 MagicNumber:TransactionBuilder.kt$TransactionBuilder$4 MagicNumber:TransactionDSLInterpreter.kt$TransactionDSL$30 - MagicNumber:TransactionUtils.kt$4 MagicNumber:TransactionVerificationException.kt$TransactionVerificationException.ConstraintPropagationRejection$3 MagicNumber:TransactionViewer.kt$TransactionViewer$15.0 MagicNumber:TransactionViewer.kt$TransactionViewer$20.0 @@ -1179,8 +1145,6 @@ MagicNumber:WebServer.kt$100.0 MagicNumber:WebServer.kt$WebServer$500 MagicNumber:WireTransaction.kt$WireTransaction$4 - MagicNumber:WorkflowTransactionBuildTutorial.kt$SubmitCompletionFlow$60 - MagicNumber:WorkflowTransactionBuildTutorial.kt$SubmitTradeApprovalFlow$60 MagicNumber:X509Utilities.kt$X509Utilities$3650 MagicNumber:errorAndTerminate.kt$10 MatchingDeclarationName:AMQPSerializerFactories.kt$net.corda.serialization.internal.amqp.AMQPSerializerFactories.kt @@ -1225,7 +1189,6 @@ MatchingDeclarationName:TestConstants.kt$net.corda.testing.core.TestConstants.kt MatchingDeclarationName:TestUtils.kt$net.corda.testing.core.TestUtils.kt MatchingDeclarationName:TransactionTypes.kt$net.corda.explorer.model.TransactionTypes.kt - MatchingDeclarationName:TutorialFlowStateMachines.kt$net.corda.docs.kotlin.tutorial.flowstatemachines.TutorialFlowStateMachines.kt MatchingDeclarationName:Utils.kt$io.cryptoblk.core.Utils.kt MatchingDeclarationName:VirtualCordapps.kt$net.corda.node.internal.cordapp.VirtualCordapps.kt ModifierOrder:NodeNamedCache.kt$DefaultNamedCacheFactory$open protected @@ -1317,13 +1280,8 @@ SpreadOperator:FlowFrameworkTripartyTests.kt$FlowFrameworkTripartyTests$(*expected) SpreadOperator:FlowLogicRefFactoryImpl.kt$FlowLogicRefFactoryImpl$(flowClass, *args) SpreadOperator:FlowOverrideTests.kt$FlowOverrideTests$(*nodeAClasses.toTypedArray()) - SpreadOperator:FlowOverrideTests.kt$FlowOverrideTests$(*nodeBClasses.toTypedArray()) SpreadOperator:FlowTestsUtils.kt$(*allSessions) SpreadOperator:FlowTestsUtils.kt$(session, *sessions) - SpreadOperator:FxTransactionBuildTutorial.kt$ForeignExchangeFlow$(*ourInputStates.toTypedArray()) - SpreadOperator:FxTransactionBuildTutorial.kt$ForeignExchangeFlow$(*ourOutputState.map { StateAndContract(it, Cash.PROGRAM_ID) }.toTypedArray()) - SpreadOperator:FxTransactionBuildTutorial.kt$ForeignExchangeFlow$(*theirInputStates.toTypedArray()) - SpreadOperator:FxTransactionBuildTutorial.kt$ForeignExchangeFlow$(*theirOutputState.map { StateAndContract(it, Cash.PROGRAM_ID) }.toTypedArray()) SpreadOperator:HTTPNetworkRegistrationService.kt$HTTPNetworkRegistrationService$(OpaqueBytes(request.encoded), "Platform-Version" to "${versionInfo.platformVersion}", "Client-Version" to versionInfo.releaseVersion, "Private-Network-Map" to (config.pnm?.toString() ?: ""), *(config.csrToken?.let { arrayOf(CENM_SUBMISSION_TOKEN to it) } ?: arrayOf())) SpreadOperator:HibernateQueryCriteriaParser.kt$AbstractQueryCriteriaParser$(*leftPredicates.toTypedArray()) SpreadOperator:HibernateQueryCriteriaParser.kt$AbstractQueryCriteriaParser$(*leftPredicates.toTypedArray(), *rightPredicates.toTypedArray()) @@ -1453,7 +1411,6 @@ ThrowsCount:StructuresTests.kt$AttachmentTest$@Test(timeout=300_000) fun `openAsJAR does not leak file handle if attachment has corrupted manifest`() ThrowsCount:TransactionVerifierServiceInternal.kt$Verifier$ private fun getUniqueContractAttachmentsByContract(): Map<ContractClassName, ContractAttachment> ThrowsCount:TransactionVerifierServiceInternal.kt$Verifier$// Using basic graph theory, a full cycle of encumbered (co-dependent) states should exist to achieve bi-directional // encumbrances. This property is important to ensure that no states involved in an encumbrance-relationship // can be spent on their own. Briefly, if any of the states is having more than one encumbrance references by // other states, a full cycle detection will fail. As a result, all of the encumbered states must be present // as "from" and "to" only once (or zero times if no encumbrance takes place). For instance, // a -> b // c -> b and a -> b // b -> a b -> c // do not satisfy the bi-directionality (full cycle) property. // // In the first example "b" appears twice in encumbrance ("to") list and "c" exists in the encumbered ("from") list only. // Due the above, one could consume "a" and "b" in the same transaction and then, because "b" is already consumed, "c" cannot be spent. // // Similarly, the second example does not form a full cycle because "a" and "c" exist in one of the lists only. // As a result, one can consume "b" and "c" in the same transactions, which will make "a" impossible to be spent. // // On other hand the following are valid constructions: // a -> b a -> c // b -> c and c -> b // c -> a b -> a // and form a full cycle, meaning that the bi-directionality property is satisfied. private fun checkBidirectionalOutputEncumbrances(statesAndEncumbrance: List<Pair<Int, Int>>) - ThrowsCount:WireTransaction.kt$WireTransaction$private fun toLedgerTransactionInternal( resolveIdentity: (PublicKey) -> Party?, resolveAttachment: (SecureHash) -> Attachment?, resolveStateRefAsSerialized: (StateRef) -> SerializedBytes<TransactionState<ContractState>>?, resolveParameters: (SecureHash?) -> NetworkParameters?, isAttachmentTrusted: (Attachment) -> Boolean ): LedgerTransaction ThrowsCount:WireTransaction.kt$WireTransaction.Companion$ @CordaInternal fun resolveStateRefBinaryComponent(stateRef: StateRef, services: ServicesForResolution): SerializedBytes<TransactionState<ContractState>>? TooGenericExceptionCaught:AMQPChannelHandler.kt$AMQPChannelHandler$ex: Exception TooGenericExceptionCaught:AMQPExceptions.kt$th: Throwable @@ -1502,7 +1459,6 @@ TooGenericExceptionCaught:DriverDSLImpl.kt$DriverDSLImpl.Companion$th: Throwable TooGenericExceptionCaught:DriverDSLImpl.kt$exception: Throwable TooGenericExceptionCaught:DriverTests.kt$DriverTests$e: Exception - TooGenericExceptionCaught:ErrorCodeLoggingTests.kt$e: Exception TooGenericExceptionCaught:ErrorHandling.kt$ErrorHandling.CheckpointAfterErrorFlow$t: Throwable TooGenericExceptionCaught:EventProcessor.kt$EventProcessor$ex: Exception TooGenericExceptionCaught:Eventually.kt$e: Exception @@ -1574,7 +1530,6 @@ TooGenericExceptionCaught:NotaryUtils.kt$e: Exception TooGenericExceptionCaught:ObjectDiffer.kt$ObjectDiffer$throwable: Exception TooGenericExceptionCaught:P2PMessagingClient.kt$P2PMessagingClient$e: Exception - TooGenericExceptionCaught:PersistentIdentityMigrationNewTableTest.kt$PersistentIdentityMigrationNewTableTest$e: Exception TooGenericExceptionCaught:PersistentUniquenessProvider.kt$PersistentUniquenessProvider$e: Exception TooGenericExceptionCaught:ProfileController.kt$ProfileController$e: Exception TooGenericExceptionCaught:PropertyValidationTest.kt$PropertyValidationTest$e: Exception @@ -1715,6 +1670,7 @@ TooManyFunctions:RPCApi.kt$net.corda.nodeapi.RPCApi.kt TooManyFunctions:RPCClientProxyHandler.kt$RPCClientProxyHandler : InvocationHandler TooManyFunctions:RPCServer.kt$RPCServer + TooManyFunctions:SSLHelper.kt$net.corda.nodeapi.internal.protonwrapper.netty.SSLHelper.kt TooManyFunctions:SerializationHelper.kt$net.corda.serialization.internal.amqp.SerializationHelper.kt TooManyFunctions:ServiceHub.kt$ServiceHub : ServicesForResolution TooManyFunctions:SignedTransaction.kt$SignedTransaction : TransactionWithSignatures @@ -1742,8 +1698,6 @@ UnusedImports:Amount.kt$import net.corda.core.crypto.CompositeKey UnusedImports:Amount.kt$import net.corda.core.identity.Party UnusedImports:DummyLinearStateSchemaV1.kt$import net.corda.core.contracts.ContractState - UnusedImports:FlowsExecutionModeRpcTest.kt$import net.corda.core.internal.packageName - UnusedImports:FlowsExecutionModeRpcTest.kt$import net.corda.finance.schemas.CashSchemaV1 UnusedImports:InternalTestUtils.kt$import java.nio.file.Files UnusedImports:InternalTestUtils.kt$import net.corda.nodeapi.internal.loadDevCaTrustStore UnusedImports:NetworkMap.kt$import net.corda.core.node.NodeInfo @@ -2012,8 +1966,6 @@ WildcardImport:CordaModule.kt$import net.corda.core.identity.* WildcardImport:CordaModule.kt$import net.corda.core.transactions.* WildcardImport:CordaRPCOps.kt$import net.corda.core.node.services.vault.* - WildcardImport:CordaRPCOpsImplTest.kt$import net.corda.core.messaging.* - WildcardImport:CordaRPCOpsImplTest.kt$import org.assertj.core.api.Assertions.* WildcardImport:CordaServiceTest.kt$import kotlin.test.* WildcardImport:CordaViewModel.kt$import tornadofx.* WildcardImport:Cordapp.kt$import net.corda.core.cordapp.Cordapp.Info.* @@ -2031,10 +1983,6 @@ WildcardImport:CryptoSignUtils.kt$import net.corda.core.crypto.* WildcardImport:CryptoUtilsTest.kt$import kotlin.test.* WildcardImport:CustomCordapp.kt$import net.corda.core.internal.* - WildcardImport:CustomVaultQuery.kt$import net.corda.core.flows.* - WildcardImport:CustomVaultQuery.kt$import net.corda.core.utilities.* - WildcardImport:CustomVaultQueryTest.kt$import net.corda.core.node.services.vault.* - WildcardImport:CustomVaultQueryTest.kt$import net.corda.finance.* WildcardImport:DBNetworkParametersStorage.kt$import javax.persistence.* WildcardImport:DBRunnerExtension.kt$import org.junit.jupiter.api.extension.* WildcardImport:DBTransactionStorage.kt$import javax.persistence.* @@ -2057,7 +2005,6 @@ WildcardImport:DeserializeSimpleTypesTests.kt$import net.corda.serialization.internal.amqp.testutils.* WildcardImport:DigitalSignatureWithCert.kt$import java.security.cert.* WildcardImport:DistributedServiceTests.kt$import net.corda.testing.core.* - WildcardImport:DoRemainingWorkTransition.kt$import net.corda.node.services.statemachine.* WildcardImport:DockerInstantiator.kt$import com.github.dockerjava.api.model.* WildcardImport:DriverDSLImpl.kt$import net.corda.testing.driver.* WildcardImport:DummyContract.kt$import net.corda.core.contracts.* @@ -2076,8 +2023,6 @@ WildcardImport:EvolutionSerializerFactoryTests.kt$import kotlin.test.* WildcardImport:EvolutionSerializerFactoryTests.kt$import net.corda.serialization.internal.amqp.testutils.* WildcardImport:Explorer.kt$import tornadofx.* - WildcardImport:FiberDeserializationCheckingInterceptor.kt$import net.corda.node.services.statemachine.* - WildcardImport:FinalityFlowMigration.kt$import net.corda.core.flows.* WildcardImport:FinalityFlowTests.kt$import net.corda.testing.core.* WildcardImport:FinalityFlowTests.kt$import net.corda.testing.node.internal.* WildcardImport:FinalityHandlerTest.kt$import net.corda.node.services.statemachine.StaffedFlowHospital.* @@ -2089,11 +2034,7 @@ WildcardImport:FlowCheckpointCordapp.kt$import net.corda.core.flows.* WildcardImport:FlowCheckpointVersionNodeStartupCheckTest.kt$import net.corda.core.flows.* WildcardImport:FlowCheckpointVersionNodeStartupCheckTest.kt$import net.corda.core.internal.* - WildcardImport:FlowCookbook.kt$import net.corda.core.contracts.* - WildcardImport:FlowCookbook.kt$import net.corda.core.flows.* WildcardImport:FlowFrameworkPersistenceTests.kt$import net.corda.testing.node.internal.* - WildcardImport:FlowFrameworkTests.kt$import net.corda.core.flows.* - WildcardImport:FlowFrameworkTests.kt$import net.corda.testing.node.internal.* WildcardImport:FlowFrameworkTripartyTests.kt$import net.corda.testing.node.internal.* WildcardImport:FlowLogicRefFactoryImpl.kt$import net.corda.core.flows.* WildcardImport:FlowMatchers.kt$import net.corda.coretesting.internal.matchers.* @@ -2101,10 +2042,7 @@ WildcardImport:FlowRetryTest.kt$import net.corda.core.flows.* WildcardImport:FlowStackSnapshotTest.kt$import net.corda.core.flows.* WildcardImport:FlowStateMachine.kt$import net.corda.core.flows.* - WildcardImport:FlowStateMachineImpl.kt$import net.corda.core.flows.* - WildcardImport:FlowStateMachineImpl.kt$import net.corda.core.internal.* WildcardImport:FlowsDrainingModeContentionTest.kt$import net.corda.core.flows.* - WildcardImport:FxTransactionBuildTutorialTest.kt$import net.corda.finance.* WildcardImport:GenericsTests.kt$import net.corda.serialization.internal.amqp.testutils.* WildcardImport:Gui.kt$import tornadofx.* WildcardImport:GuiUtilities.kt$import tornadofx.* @@ -2121,8 +2059,6 @@ WildcardImport:HibernateQueryCriteriaParser.kt$import net.corda.core.node.services.vault.EqualityComparisonOperator.* WildcardImport:HibernateQueryCriteriaParser.kt$import net.corda.core.node.services.vault.LikenessOperator.* WildcardImport:HibernateStatistics.kt$import org.hibernate.stat.* - WildcardImport:IOUContract.kt$import net.corda.core.contracts.* - WildcardImport:IOUFlowResponder.kt$import net.corda.core.flows.* WildcardImport:IRS.kt$import net.corda.core.contracts.* WildcardImport:IRS.kt$import net.corda.finance.contracts.* WildcardImport:IRSState.kt$import net.corda.core.contracts.* @@ -2169,7 +2105,6 @@ WildcardImport:JarSignatureCollectorTest.kt$import net.corda.core.internal.* WildcardImport:KeyStoreUtilities.kt$import java.security.* WildcardImport:KeyStoreUtilities.kt$import net.corda.core.internal.* - WildcardImport:KotlinIntegrationTestingTutorial.kt$import net.corda.testing.core.* WildcardImport:Kryo.kt$import com.esotericsoftware.kryo.* WildcardImport:Kryo.kt$import net.corda.core.transactions.* WildcardImport:KryoStreamsTest.kt$import java.io.* @@ -2420,8 +2355,6 @@ WildcardImport:SignedTransaction.kt$import net.corda.core.contracts.* WildcardImport:SignedTransaction.kt$import net.corda.core.crypto.* WildcardImport:SimpleMQClient.kt$import org.apache.activemq.artemis.api.core.client.* - WildcardImport:SingleThreadedStateMachineManager.kt$import net.corda.core.internal.* - WildcardImport:SingleThreadedStateMachineManager.kt$import net.corda.node.services.statemachine.interceptors.* WildcardImport:SpringDriver.kt$import net.corda.testing.node.internal.* WildcardImport:StandaloneCordaRPClientTest.kt$import net.corda.core.messaging.* WildcardImport:StandaloneCordaRPClientTest.kt$import net.corda.core.node.services.vault.* @@ -2473,8 +2406,6 @@ WildcardImport:TransactionViewer.kt$import net.corda.client.jfx.utils.* WildcardImport:TransactionViewer.kt$import net.corda.core.contracts.* WildcardImport:TransactionViewer.kt$import tornadofx.* - WildcardImport:TutorialContract.kt$import net.corda.core.contracts.* - WildcardImport:TutorialTestDSL.kt$import net.corda.testing.core.* WildcardImport:TwoPartyDealFlow.kt$import net.corda.core.flows.* WildcardImport:TwoPartyTradeFlow.kt$import net.corda.core.contracts.* WildcardImport:TwoPartyTradeFlow.kt$import net.corda.core.flows.* @@ -2502,7 +2433,6 @@ WildcardImport:ValidatingNotaryServiceTests.kt$import net.corda.core.flows.* WildcardImport:ValidatingNotaryServiceTests.kt$import net.corda.testing.node.internal.* WildcardImport:VaultFiller.kt$import net.corda.core.contracts.* - WildcardImport:VaultFlowTest.kt$import net.corda.core.flows.* WildcardImport:VaultQueryExceptionsTests.kt$import net.corda.core.node.services.* WildcardImport:VaultQueryExceptionsTests.kt$import net.corda.core.node.services.vault.* WildcardImport:VaultQueryExceptionsTests.kt$import net.corda.core.node.services.vault.QueryCriteria.* diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/SSLHelper.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/SSLHelper.kt index 9efd7d10dc..233b19a712 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/SSLHelper.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/SSLHelper.kt @@ -200,10 +200,7 @@ internal fun createClientSslHelper(target: NetworkHostAndPort, expectedRemoteLegalNames: Set, keyManagerFactory: KeyManagerFactory, trustManagerFactory: TrustManagerFactory): SslHandler { - val sslContext = SSLContext.getInstance("TLS") - val keyManagers = keyManagerFactory.keyManagers - val trustManagers = trustManagerFactory.trustManagers.filterIsInstance(X509ExtendedTrustManager::class.java).map { LoggingTrustManagerWrapper(it) }.toTypedArray() - sslContext.init(keyManagers, trustManagers, newSecureRandom()) + val sslContext = createAndInitSslContext(keyManagerFactory, trustManagerFactory) val sslEngine = sslContext.createSSLEngine(target.host, target.port) sslEngine.useClientMode = true sslEngine.enabledProtocols = ArtemisTcpTransport.TLS_VERSIONS.toTypedArray() @@ -239,10 +236,7 @@ internal fun createClientOpenSslHandler(target: NetworkHostAndPort, internal fun createServerSslHandler(keyStore: CertificateStore, keyManagerFactory: KeyManagerFactory, trustManagerFactory: TrustManagerFactory): SslHandler { - val sslContext = SSLContext.getInstance("TLS") - val keyManagers = keyManagerFactory.keyManagers - val trustManagers = trustManagerFactory.trustManagers.filterIsInstance(X509ExtendedTrustManager::class.java).map { LoggingTrustManagerWrapper(it) }.toTypedArray() - sslContext.init(keyManagers, trustManagers, newSecureRandom()) + val sslContext = createAndInitSslContext(keyManagerFactory, trustManagerFactory) val sslEngine = sslContext.createSSLEngine() sslEngine.useClientMode = false sslEngine.needClientAuth = true @@ -256,6 +250,15 @@ internal fun createServerSslHandler(keyStore: CertificateStore, return SslHandler(sslEngine, false, LoggingImmediateExecutor) } +fun createAndInitSslContext(keyManagerFactory: KeyManagerFactory, trustManagerFactory: TrustManagerFactory): SSLContext { + val sslContext = SSLContext.getInstance("TLS") + val keyManagers = keyManagerFactory.keyManagers + val trustManagers = trustManagerFactory.trustManagers.filterIsInstance(X509ExtendedTrustManager::class.java) + .map { LoggingTrustManagerWrapper(it) }.toTypedArray() + sslContext.init(keyManagers, trustManagers, newSecureRandom()) + return sslContext +} + @VisibleForTesting fun initialiseTrustStoreAndEnableCrlChecking(trustStore: CertificateStore, revocationConfig: RevocationConfig): ManagerFactoryParameters { val pkixParams = PKIXBuilderParameters(trustStore.value.internal, X509CertSelector()) diff --git a/node/src/integration-test/java/net/corda/node/amqp/NioSslClient.java b/node/src/integration-test/java/net/corda/node/amqp/NioSslClient.java new file mode 100644 index 0000000000..3a1de2d0a1 --- /dev/null +++ b/node/src/integration-test/java/net/corda/node/amqp/NioSslClient.java @@ -0,0 +1,216 @@ +package net.corda.node.amqp; + +import net.corda.nodeapi.internal.protonwrapper.netty.SSLHelperKt; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLEngineResult; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLSession; +import javax.net.ssl.TrustManagerFactory; + +/** + * An SSL/TLS client that connects to a server using its IP address and port. + *

+ * After initialization of a {@link NioSslClient} object, {@link NioSslClient#connect()} should be called, + * in order to establish connection with the server. + *

+ * Just like {@link NioSslPeer#read(SocketChannel, SSLEngine)} it uses inner class' socket channel + * and engine and should not be used by the client. {@link NioSslClient#read()} should be called instead. + * + * @param engine - the engine used for encryption/decryption of the data exchanged between the two peers. + */ + @Override + protected void read(SocketChannel socketChannel, SSLEngine engine) throws Exception { + + log.debug("About to read from the server..."); + + peerNetData.clear(); + int waitToReadMillis = 50; + boolean exitReadLoop = false; + while (!exitReadLoop) { + int bytesRead = socketChannel.read(peerNetData); + if (bytesRead > 0) { + peerNetData.flip(); + while (peerNetData.hasRemaining()) { + peerAppData.clear(); + SSLEngineResult result = engine.unwrap(peerNetData, peerAppData); + switch (result.getStatus()) { + case OK: + peerAppData.flip(); + log.debug("Server response: " + peerAppDataAsString()); + exitReadLoop = true; + break; + case BUFFER_OVERFLOW: + peerAppData = enlargeApplicationBuffer(engine, peerAppData); + break; + case BUFFER_UNDERFLOW: + peerNetData = handleBufferUnderflow(engine, peerNetData); + break; + case CLOSED: + closeConnection(socketChannel, engine); + return; + default: + throw new IllegalStateException("Invalid SSL status: " + result.getStatus()); + } + } + } else if (bytesRead < 0) { + handleEndOfStream(socketChannel, engine); + return; + } + Thread.sleep(waitToReadMillis); + } + } + + /** + * Should be called when the client wants to explicitly close the connection to the server. + * + * @throws IOException if an I/O error occurs to the socket channel. + */ + public void shutdown() throws IOException { + log.debug("About to close connection with the server..."); + closeConnection(socketChannel, engine); + executor.shutdown(); + log.debug("Goodbye!"); + } +} \ No newline at end of file diff --git a/node/src/integration-test/java/net/corda/node/amqp/NioSslPeer.java b/node/src/integration-test/java/net/corda/node/amqp/NioSslPeer.java new file mode 100644 index 0000000000..060f918846 --- /dev/null +++ b/node/src/integration-test/java/net/corda/node/amqp/NioSslPeer.java @@ -0,0 +1,329 @@ +package net.corda.node.amqp; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLEngineResult; +import javax.net.ssl.SSLEngineResult.HandshakeStatus; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLSession; +import java.io.IOException; +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; +import java.util.Arrays; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * A class that represents an SSL/TLS peer, and can be extended to create a client or a server. + *

+ * It makes use of the JSSE framework, and specifically the {@link SSLEngine} logic, which + * is described by Oracle as "an advanced API, not appropriate for casual use", since + * it requires the user to implement much of the communication establishment procedure himself. + * More information about it can be found here: http://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/JSSERefGuide.html#SSLEngine + *

+ * {@link NioSslPeer} implements the handshake protocol, required to establish a connection between two peers, + * which is common for both client and server and provides the abstract {@link NioSslPeer#read(SocketChannel, SSLEngine)} and + * {@link NioSslPeer#write(SocketChannel, SSLEngine, String)} methods, that need to be implemented by the specific SSL/TLS peer + * that is going to extend this class. + * + * @author Alex Karnezis + */ +public abstract class NioSslPeer { + + /** + * Class' logger. + */ + protected final Logger log = LoggerFactory.getLogger(getClass()); + + /** + * Will contain this peer's application data in plaintext, that will be later encrypted + * using {@link SSLEngine#wrap(ByteBuffer, ByteBuffer)} and sent to the other peer. This buffer can typically + * be of any size, as long as it is large enough to contain this peer's outgoing messages. + * If this peer tries to send a message bigger than buffer's capacity a {@link BufferOverflowException} + * will be thrown. + */ + protected ByteBuffer myAppData; + + /** + * Will contain this peer's encrypted data, that will be generated after {@link SSLEngine#wrap(ByteBuffer, ByteBuffer)} + * is applied on {@link NioSslPeer#myAppData}. It should be initialized using {@link SSLSession#getPacketBufferSize()}, + * which returns the size up to which, SSL/TLS packets will be generated from the engine under a session. + * All SSLEngine network buffers should be sized at least this large to avoid insufficient space problems when performing wrap and unwrap calls. + */ + protected ByteBuffer myNetData; + + /** + * Will contain the other peer's (decrypted) application data. It must be large enough to hold the application data + * from any peer. Can be initialized with {@link SSLSession#getApplicationBufferSize()} for an estimation + * of the other peer's application data and should be enlarged if this size is not enough. + */ + protected ByteBuffer peerAppData; + + /** + * Will contain the other peer's encrypted data. The SSL/TLS protocols specify that implementations should produce packets containing at most 16 KB of plaintext, + * so a buffer sized to this value should normally cause no capacity problems. However, some implementations violate the specification and generate large records up to 32 KB. + * If the {@link SSLEngine#unwrap(ByteBuffer, ByteBuffer)} detects large inbound packets, the buffer sizes returned by SSLSession will be updated dynamically, so the this peer + * should check for overflow conditions and enlarge the buffer using the session's (updated) buffer size. + */ + protected ByteBuffer peerNetData; + + /** + * Will be used to execute tasks that may emerge during handshake in parallel with the server's main thread. + */ + protected ExecutorService executor = Executors.newSingleThreadExecutor(); + + protected abstract void read(SocketChannel socketChannel, SSLEngine engine) throws Exception; + + protected abstract void write(SocketChannel socketChannel, SSLEngine engine, String message) throws Exception; + + /** + * Implements the handshake protocol between two peers, required for the establishment of the SSL/TLS connection. + * During the handshake, encryption configuration information - such as the list of available cipher suites - will be exchanged + * and if the handshake is successful will lead to an established SSL/TLS session. + * + *

+ * A typical handshake will usually contain the following steps: + * + *

    + *
  • 1. wrap: ClientHello
  • + *
  • 2. unwrap: ServerHello/Cert/ServerHelloDone
  • + *
  • 3. wrap: ClientKeyExchange
  • + *
  • 4. wrap: ChangeCipherSpec
  • + *
  • 5. wrap: Finished
  • + *
  • 6. unwrap: ChangeCipherSpec
  • + *
  • 7. unwrap: Finished
  • + *
+ *

+ * Handshake is also used during the end of the session, in order to properly close the connection between the two peers. + * A proper connection close will typically include the one peer sending a CLOSE message to another, and then wait for + * the other's CLOSE message to close the transport link. The other peer from his perspective would read a CLOSE message + * from his peer and then enter the handshake procedure to send his own CLOSE message as well. + * + * @param socketChannel - the socket channel that connects the two peers. + * @param engine - the engine that will be used for encryption/decryption of the data exchanged with the other peer. + * @return True if the connection handshake was successful or false if an error occurred. + * @throws IOException - if an error occurs during read/write to the socket channel. + */ + protected boolean doHandshake(SocketChannel socketChannel, SSLEngine engine) throws IOException { + + log.debug("About to do handshake..."); + + SSLEngineResult result; + HandshakeStatus handshakeStatus; + + // NioSslPeer's fields myAppData and peerAppData are supposed to be large enough to hold all message data the peer + // will send and expects to receive from the other peer respectively. Since the messages to be exchanged will usually be less + // than 16KB long the capacity of these fields should also be smaller. Here we initialize these two local buffers + // to be used for the handshake, while keeping client's buffers at the same size. + int appBufferSize = engine.getSession().getApplicationBufferSize(); + ByteBuffer myAppData = ByteBuffer.allocate(appBufferSize); + ByteBuffer peerAppData = ByteBuffer.allocate(appBufferSize); + myNetData.clear(); + peerNetData.clear(); + + handshakeStatus = engine.getHandshakeStatus(); + while (handshakeStatus != SSLEngineResult.HandshakeStatus.FINISHED && handshakeStatus != SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING) { + switch (handshakeStatus) { + case NEED_UNWRAP: + if (socketChannel.read(peerNetData) < 0) { + if (engine.isInboundDone() && engine.isOutboundDone()) { + return false; + } + try { + engine.closeInbound(); + } catch (SSLException e) { + log.error("This engine was forced to close inbound, without having received the proper SSL/TLS close " + + "notification message from the peer, due to end of stream.", e); + } + engine.closeOutbound(); + // After closeOutbound the engine will be set to WRAP state, in order to try to send a close message to the client. + handshakeStatus = engine.getHandshakeStatus(); + break; + } + peerNetData.flip(); + try { + result = engine.unwrap(peerNetData, peerAppData); + peerNetData.compact(); + handshakeStatus = result.getHandshakeStatus(); + } catch (SSLException sslException) { + log.error("A problem was encountered while processing the data that caused the SSLEngine to abort." + + " Will try to properly close connection...", sslException); + engine.closeOutbound(); + handshakeStatus = engine.getHandshakeStatus(); + break; + } + switch (result.getStatus()) { + case OK: + break; + case BUFFER_OVERFLOW: + // Will occur when peerAppData's capacity is smaller than the data derived from peerNetData's unwrap. + peerAppData = enlargeApplicationBuffer(engine, peerAppData); + break; + case BUFFER_UNDERFLOW: + // Will occur either when no data was read from the peer or when the peerNetData buffer was too small to hold all peer's data. + peerNetData = handleBufferUnderflow(engine, peerNetData); + break; + case CLOSED: + if (engine.isOutboundDone()) { + return false; + } else { + engine.closeOutbound(); + handshakeStatus = engine.getHandshakeStatus(); + break; + } + default: + throw new IllegalStateException("Invalid SSL status: " + result.getStatus()); + } + break; + case NEED_WRAP: + myNetData.clear(); + try { + result = engine.wrap(myAppData, myNetData); + handshakeStatus = result.getHandshakeStatus(); + } catch (SSLException sslException) { + log.error("A problem was encountered while processing the data that caused the SSLEngine to abort." + + "Will try to properly close connection...", sslException); + engine.closeOutbound(); + handshakeStatus = engine.getHandshakeStatus(); + break; + } + switch (result.getStatus()) { + case OK : + myNetData.flip(); + while (myNetData.hasRemaining()) { + socketChannel.write(myNetData); + } + break; + case BUFFER_OVERFLOW: + // Will occur if there is not enough space in myNetData buffer to write all the data that would be generated by the method wrap. + // Since myNetData is set to session's packet size we should not get to this point because SSLEngine is supposed + // to produce messages smaller or equal to that, but a general handling would be the following: + myNetData = enlargePacketBuffer(engine, myNetData); + break; + case BUFFER_UNDERFLOW: + throw new SSLException("Buffer underflow occurred after a wrap. I don't think we should ever get here."); + case CLOSED: + try { + myNetData.flip(); + while (myNetData.hasRemaining()) { + socketChannel.write(myNetData); + } + // At this point the handshake status will probably be NEED_UNWRAP so we make sure that peerNetData is clear to read. + peerNetData.clear(); + } catch (Exception e) { + log.error("Failed to send server's CLOSE message due to socket channel's failure."); + handshakeStatus = engine.getHandshakeStatus(); + } + break; + default: + throw new IllegalStateException("Invalid SSL status: " + result.getStatus()); + } + break; + case NEED_TASK: + Runnable task; + while ((task = engine.getDelegatedTask()) != null) { + executor.execute(task); + } + handshakeStatus = engine.getHandshakeStatus(); + break; + default: + throw new IllegalStateException("Invalid SSL status: " + handshakeStatus); + } + } + log.debug("Handshake status: " + handshakeStatus); + + return true; + + } + + protected ByteBuffer enlargePacketBuffer(SSLEngine engine, ByteBuffer buffer) { + return enlargeBuffer(buffer, engine.getSession().getPacketBufferSize()); + } + + protected ByteBuffer enlargeApplicationBuffer(SSLEngine engine, ByteBuffer buffer) { + return enlargeBuffer(buffer, engine.getSession().getApplicationBufferSize()); + } + + /** + * Compares sessionProposedCapacity with buffer's capacity. If buffer's capacity is smaller, + * returns a buffer with the proposed capacity. If it's equal or larger, returns a buffer + * with capacity twice the size of the initial one. + * + * @param buffer - the buffer to be enlarged. + * @param sessionProposedCapacity - the minimum size of the new buffer, proposed by {@link SSLSession}. + * @return A new buffer with a larger capacity. + */ + protected ByteBuffer enlargeBuffer(ByteBuffer buffer, int sessionProposedCapacity) { + if (sessionProposedCapacity > buffer.capacity()) { + buffer = ByteBuffer.allocate(sessionProposedCapacity); + } else { + buffer = ByteBuffer.allocate(buffer.capacity() * 2); + } + return buffer; + } + + /** + * Handles {@link SSLEngineResult.Status#BUFFER_UNDERFLOW}. Will check if the buffer is already filled, and if there is no space problem + * will return the same buffer, so the client tries to read again. If the buffer is already filled will try to enlarge the buffer either to + * session's proposed size or to a larger capacity. A buffer underflow can happen only after an unwrap, so the buffer will always be a + * peerNetData buffer. + * + * @param buffer - will always be peerNetData buffer. + * @param engine - the engine used for encryption/decryption of the data exchanged between the two peers. + * @return The same buffer if there is no space problem or a new buffer with the same data but more space. + */ + protected ByteBuffer handleBufferUnderflow(SSLEngine engine, ByteBuffer buffer) { + if (engine.getSession().getPacketBufferSize() < buffer.limit()) { + return buffer; + } else { + ByteBuffer replaceBuffer = enlargePacketBuffer(engine, buffer); + buffer.flip(); + replaceBuffer.put(buffer); + return replaceBuffer; + } + } + + /** + * This method should be called when this peer wants to explicitly close the connection + * or when a close message has arrived from the other peer, in order to provide an orderly shutdown. + *

+ * It first calls {@link SSLEngine#closeOutbound()} which prepares this peer to send its own close message and + * sets {@link SSLEngine} to the NEED_WRAP state. Then, it delegates the exchange of close messages + * to the handshake method and finally, it closes socket channel. + * + * @param socketChannel - the transport link used between the two peers. + * @param engine - the engine used for encryption/decryption of the data exchanged between the two peers. + * @throws IOException if an I/O error occurs to the socket channel. + */ + protected void closeConnection(SocketChannel socketChannel, SSLEngine engine) throws IOException { + engine.closeOutbound(); + doHandshake(socketChannel, engine); + socketChannel.close(); + } + + /** + * In addition to orderly shutdowns, an unorderly shutdown may occur, when the transport link (socket channel) + * is severed before close messages are exchanged. This may happen by getting an -1 or {@link IOException} + * when trying to read from the socket channel, or an {@link IOException} when trying to write to it. + * In both cases {@link SSLEngine#closeInbound()} should be called and then try to follow the standard procedure. + * + * @param socketChannel - the transport link used between the two peers. + * @param engine - the engine used for encryption/decryption of the data exchanged between the two peers. + * @throws IOException if an I/O error occurs to the socket channel. + */ + protected void handleEndOfStream(SocketChannel socketChannel, SSLEngine engine) throws IOException { + try { + engine.closeInbound(); + } catch (Exception e) { + log.error("This engine was forced to close inbound, without having received the proper SSL/TLS close notification message from the peer, due to end of stream."); + } + closeConnection(socketChannel, engine); + } + + protected String peerAppDataAsString() { + return new String(Arrays.copyOf(peerAppData.array(), peerAppData.limit())); + } +} \ No newline at end of file diff --git a/node/src/integration-test/java/net/corda/node/amqp/NioSslServer.java b/node/src/integration-test/java/net/corda/node/amqp/NioSslServer.java new file mode 100644 index 0000000000..d316c62c0b --- /dev/null +++ b/node/src/integration-test/java/net/corda/node/amqp/NioSslServer.java @@ -0,0 +1,263 @@ +package net.corda.node.amqp; + +import net.corda.nodeapi.internal.protonwrapper.netty.SSLHelperKt; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.nio.channels.spi.SelectorProvider; +import java.time.Duration; +import java.util.Iterator; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLEngineResult; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLSession; +import javax.net.ssl.TrustManagerFactory; + +/** + * An SSL/TLS server, that will listen to a specific address and port and serve SSL/TLS connections + * compatible with the protocol it applies. + *

+ * After initialization {@link NioSslServer#start()} should be called so the server starts to listen to + * new connection requests. At this point, start is blocking, so, in order to be able to gracefully stop + * the server, a {@link Runnable} containing a server object should be created. This runnable should + * start the server in its run method and also provide a stop method, which will call {@link NioSslServer#stop()}. + *

+ * NioSslServer makes use of Java NIO, and specifically listens to new connection requests with a {@link ServerSocketChannel}, which will + * create new {@link SocketChannel}s and a {@link Selector} which serves all the connections in one thread. + * + * @author Alex Karnezis + */ +public class NioSslServer extends NioSslPeer { + + private final Duration handshakeDelay; + /** + * Declares if the server is active to serve and create new connections. + */ + private boolean active; + + /** + * The context will be initialized with a specific SSL/TLS protocol and will then be used + * to create {@link SSLEngine} classes for each new connection that arrives to the server. + */ + private final SSLContext context; + + /** + * A part of Java NIO that will be used to serve all connections to the server in one thread. + */ + private final Selector selector; + + + /** + * Server is designed to apply an SSL/TLS protocol and listen to an IP address and port. + * + * @param hostAddress - the IP address this server will listen to. + * @param port - the port this server will listen to. + * @param handshakeDelay - if not [null] specifies for how long the handshake should be delayed + */ + public NioSslServer(KeyManagerFactory keyManagerFactory, TrustManagerFactory trustManagerFactory, String hostAddress, int port, + Duration handshakeDelay) throws Exception { + + context = SSLHelperKt.createAndInitSslContext(keyManagerFactory, trustManagerFactory); + + SSLSession dummySession = context.createSSLEngine().getSession(); + myAppData = ByteBuffer.allocate(dummySession.getApplicationBufferSize()); + myNetData = ByteBuffer.allocate(dummySession.getPacketBufferSize()); + peerAppData = ByteBuffer.allocate(dummySession.getApplicationBufferSize()); + peerNetData = ByteBuffer.allocate(dummySession.getPacketBufferSize()); + dummySession.invalidate(); + + selector = SelectorProvider.provider().openSelector(); + ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); + serverSocketChannel.configureBlocking(false); + serverSocketChannel.socket().bind(new InetSocketAddress(hostAddress, port)); + serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); + this.handshakeDelay = handshakeDelay; + + active = true; + + } + + /** + * Should be called in order the server to start listening to new connections. + * This method will run in a loop as long as the server is active. In order to stop the server + * you should use {@link NioSslServer#stop()} which will set it to inactive state + * and also wake up the listener, which may be in blocking select() state. + */ + public void start() throws Exception { + + log.debug("Initialized and waiting for new connections..."); + + while (isActive()) { + selector.select(); + Iterator selectedKeys = selector.selectedKeys().iterator(); + while (selectedKeys.hasNext()) { + SelectionKey key = selectedKeys.next(); + selectedKeys.remove(); + if (!key.isValid()) { + continue; + } + if (key.isAcceptable()) { + accept(key); + } else if (key.isReadable()) { + read((SocketChannel) key.channel(), (SSLEngine) key.attachment()); + } + } + } + + log.debug("Goodbye!"); + + } + + /** + * Sets the server to an inactive state, in order to exit the reading loop in {@link NioSslServer#start()} + * and also wakes up the selector, which may be in select() blocking state. + */ + public void stop() { + log.debug("Will now close server..."); + active = false; + executor.shutdown(); + selector.wakeup(); + } + + /** + * Will be called after a new connection request arrives to the server. Creates the {@link SocketChannel} that will + * be used as the network layer link, and the {@link SSLEngine} that will encrypt and decrypt all the data + * that will be exchanged during the session with this specific client. + * + * @param key - the key dedicated to the {@link ServerSocketChannel} used by the server to listen to new connection requests. + */ + private void accept(SelectionKey key) throws Exception { + + log.debug("New connection request!"); + + SocketChannel socketChannel = ((ServerSocketChannel) key.channel()).accept(); + socketChannel.configureBlocking(false); + + SSLEngine engine = context.createSSLEngine(); + engine.setUseClientMode(false); + // Demand client to present its certificate + engine.setNeedClientAuth(true); + engine.beginHandshake(); + + if (handshakeDelay != null) { + log.info("Deliberately sleeping during handshake for: " + handshakeDelay); + Thread.sleep(handshakeDelay.toMillis()); + } + + if (doHandshake(socketChannel, engine)) { + socketChannel.register(selector, SelectionKey.OP_READ, engine); + } else { + socketChannel.close(); + log.debug("Connection closed due to handshake failure."); + } + } + + /** + * Will be called by the selector when the specific socket channel has data to be read. + * As soon as the server reads these data, it will call {@link NioSslServer#write(SocketChannel, SSLEngine, String)} + * to send back a trivial response. + * + * @param socketChannel - the transport link used between the two peers. + * @param engine - the engine used for encryption/decryption of the data exchanged between the two peers. + * @throws IOException if an I/O error occurs to the socket channel. + */ + @Override + protected void read(SocketChannel socketChannel, SSLEngine engine) throws IOException { + + log.debug("About to read from a client..."); + + peerNetData.clear(); + int bytesRead = socketChannel.read(peerNetData); + if (bytesRead > 0) { + peerNetData.flip(); + while (peerNetData.hasRemaining()) { + peerAppData.clear(); + SSLEngineResult result = engine.unwrap(peerNetData, peerAppData); + switch (result.getStatus()) { + case OK: + peerAppData.flip(); + log.debug("Incoming message: " + peerAppDataAsString()); + break; + case BUFFER_OVERFLOW: + peerAppData = enlargeApplicationBuffer(engine, peerAppData); + break; + case BUFFER_UNDERFLOW: + peerNetData = handleBufferUnderflow(engine, peerNetData); + break; + case CLOSED: + log.debug("Client wants to close connection..."); + closeConnection(socketChannel, engine); + log.debug("Goodbye client!"); + return; + default: + throw new IllegalStateException("Invalid SSL status: " + result.getStatus()); + } + } + + write(socketChannel, engine, "Hello! I am your server!"); + + } else if (bytesRead < 0) { + log.error("Received end of stream. Will try to close connection with client..."); + handleEndOfStream(socketChannel, engine); + log.debug("Goodbye client!"); + } + } + + /** + * Will send a message back to a client. + * + * @param message - the message to be sent. + * @throws IOException if an I/O error occurs to the socket channel. + */ + @Override + protected void write(SocketChannel socketChannel, SSLEngine engine, String message) throws IOException { + + log.debug("About to write to a client..."); + + myAppData.clear(); + myAppData.put(message.getBytes()); + myAppData.flip(); + while (myAppData.hasRemaining()) { + // The loop has a meaning for (outgoing) messages larger than 16KB. + // Every wrap call will remove 16KB from the original message and send it to the remote peer. + myNetData.clear(); + SSLEngineResult result = engine.wrap(myAppData, myNetData); + switch (result.getStatus()) { + case OK: + myNetData.flip(); + while (myNetData.hasRemaining()) { + socketChannel.write(myNetData); + } + log.debug("Message sent to the client: " + message); + break; + case BUFFER_OVERFLOW: + myNetData = enlargePacketBuffer(engine, myNetData); + break; + case BUFFER_UNDERFLOW: + throw new SSLException("Buffer underflow occurred after a wrap. I don't think we should ever get here."); + case CLOSED: + closeConnection(socketChannel, engine); + return; + default: + throw new IllegalStateException("Invalid SSL status: " + result.getStatus()); + } + } + } + + /** + * Determines if the the server is active or not. + * + * @return if the server is active or not. + */ + public boolean isActive() { + return active; + } +} \ No newline at end of file diff --git a/node/src/integration-test/java/net/corda/node/amqp/ServerThread.java b/node/src/integration-test/java/net/corda/node/amqp/ServerThread.java new file mode 100644 index 0000000000..ee108893fb --- /dev/null +++ b/node/src/integration-test/java/net/corda/node/amqp/ServerThread.java @@ -0,0 +1,69 @@ +package net.corda.node.amqp; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nullable; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.TrustManagerFactory; +import java.time.Duration; + +/** + * This class provides a runnable that can be used to initialize a {@link NioSslServer} thread. + *

+ * Run starts the server, which will start listening to the configured IP address and port for + * new SSL/TLS connections and serve the ones already connected to it. + *

+ * Also a stop method is provided in order to gracefully close the server and stop the thread. + * + * @author Alex Karnezis + */ +public class ServerThread implements AutoCloseable { + + private final static Logger log = LoggerFactory.getLogger(ServerThread.class); + + private static final long JOIN_TIMEOUT_MS = 10000; + + private final NioSslServer server; + + private Thread serverThread; + + public ServerThread(KeyManagerFactory keyManagerFactory, TrustManagerFactory trustManagerFactory, int port) throws Exception { + this(keyManagerFactory, trustManagerFactory, port, null); + } + + public ServerThread(KeyManagerFactory keyManagerFactory, TrustManagerFactory trustManagerFactory, int port, @Nullable Duration handshakeDelay) throws Exception { + server = new NioSslServer(keyManagerFactory, trustManagerFactory, "localhost", port, handshakeDelay); + } + + public void start() { + + Runnable serverRunnable = () -> { + try { + server.start(); + } catch (Exception e) { + log.error("Exception starting server", e); + } + }; + + serverThread = new Thread(serverRunnable, this.getClass().getSimpleName() + "-ServerThread"); + serverThread.start(); + } + + /** + * Should be called in order to gracefully stop the server. + */ + public void stop() throws InterruptedException { + server.stop(); + serverThread.join(JOIN_TIMEOUT_MS); + } + + public boolean isActive() { + return server.isActive(); + } + + @Override + public void close() throws Exception { + stop(); + } +} \ No newline at end of file diff --git a/node/src/integration-test/kotlin/net/corda/node/amqp/AMQPClientSslErrorsTest.kt b/node/src/integration-test/kotlin/net/corda/node/amqp/AMQPClientSslErrorsTest.kt new file mode 100644 index 0000000000..27a8a28189 --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/node/amqp/AMQPClientSslErrorsTest.kt @@ -0,0 +1,208 @@ +package net.corda.node.amqp + +import com.nhaarman.mockito_kotlin.doReturn +import com.nhaarman.mockito_kotlin.mock +import com.nhaarman.mockito_kotlin.whenever +import net.corda.core.internal.div +import net.corda.core.toFuture +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.contextLogger +import net.corda.core.utilities.seconds +import net.corda.coretesting.internal.stubs.CertificateStoreStubs +import net.corda.node.services.config.NodeConfiguration +import net.corda.node.services.config.configureWithDevSSLCertificate +import net.corda.nodeapi.internal.protonwrapper.netty.AMQPClient +import net.corda.nodeapi.internal.protonwrapper.netty.AMQPConfiguration +import net.corda.nodeapi.internal.protonwrapper.netty.init +import net.corda.nodeapi.internal.protonwrapper.netty.initialiseTrustStoreAndEnableCrlChecking +import net.corda.nodeapi.internal.protonwrapper.netty.toRevocationConfig +import net.corda.testing.core.ALICE_NAME +import net.corda.testing.core.BOB_NAME +import net.corda.testing.driver.internal.incrementalPortAllocation +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import javax.net.ssl.KeyManagerFactory +import javax.net.ssl.TrustManagerFactory +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +/** + * This test verifies some edge case scenarios like handshake timeouts when [AMQPClient] connected to the server + * + * In order to have control over handshake internals a simple TLS server is created which may have a configurable handshake delay. + */ +@RunWith(Parameterized::class) +class AMQPClientSslErrorsTest(@Suppress("unused") private val iteration: Int) { + + companion object { + private const val MAX_MESSAGE_SIZE = 10 * 1024 + private val log = contextLogger() + + @JvmStatic + @Parameterized.Parameters(name = "iteration = {0}") + fun iterations(): Iterable> { + // It is possible to change this value to a greater number + // to ensure that the test is not flaking when executed on CI + val repsCount = 1 + return (1..repsCount).map { arrayOf(it) } + } + } + + @Rule + @JvmField + val temporaryFolder = TemporaryFolder() + + private val portAllocation = incrementalPortAllocation() + + private lateinit var serverKeyManagerFactory: KeyManagerFactory + private lateinit var serverTrustManagerFactory: TrustManagerFactory + + private lateinit var clientKeyManagerFactory: KeyManagerFactory + private lateinit var clientTrustManagerFactory: TrustManagerFactory + + private lateinit var clientAmqpConfig: AMQPConfiguration + + @Before + fun setup() { + setupServerCertificates() + setupClientCertificates() + } + + private fun setupServerCertificates() { + val baseDirectory = temporaryFolder.root.toPath() / "server" + val certificatesDirectory = baseDirectory / "certificates" + val p2pSslConfiguration = CertificateStoreStubs.P2P.withCertificatesDirectory(certificatesDirectory) + val signingCertificateStore = CertificateStoreStubs.Signing.withCertificatesDirectory(certificatesDirectory) + val serverConfig = mock().also { + doReturn(baseDirectory).whenever(it).baseDirectory + doReturn(certificatesDirectory).whenever(it).certificatesDirectory + doReturn(ALICE_NAME).whenever(it).myLegalName + doReturn(p2pSslConfiguration).whenever(it).p2pSslOptions + doReturn(signingCertificateStore).whenever(it).signingCertificateStore + } + serverConfig.configureWithDevSSLCertificate() + val keyStore = serverConfig.p2pSslOptions.keyStore.get() + val serverAmqpConfig = object : AMQPConfiguration { + override val keyStore = keyStore + override val trustStore = serverConfig.p2pSslOptions.trustStore.get() + override val revocationConfig = true.toRevocationConfig() + override val maxMessageSize: Int = MAX_MESSAGE_SIZE + } + + serverKeyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()) + serverTrustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) + + serverKeyManagerFactory.init(keyStore) + serverTrustManagerFactory.init(initialiseTrustStoreAndEnableCrlChecking(serverAmqpConfig.trustStore, serverAmqpConfig.revocationConfig)) + } + + private fun setupClientCertificates() { + val baseDirectory = temporaryFolder.root.toPath() / "client" + val certificatesDirectory = baseDirectory / "certificates" + val p2pSslConfiguration = CertificateStoreStubs.P2P.withCertificatesDirectory(certificatesDirectory) + val signingCertificateStore = CertificateStoreStubs.Signing.withCertificatesDirectory(certificatesDirectory) + val clientConfig = mock().also { + doReturn(baseDirectory).whenever(it).baseDirectory + doReturn(certificatesDirectory).whenever(it).certificatesDirectory + doReturn(BOB_NAME).whenever(it).myLegalName + doReturn(p2pSslConfiguration).whenever(it).p2pSslOptions + doReturn(signingCertificateStore).whenever(it).signingCertificateStore + doReturn(true).whenever(it).crlCheckSoftFail + } + clientConfig.configureWithDevSSLCertificate() + //val nodeCert = (signingCertificateStore to p2pSslConfiguration).recreateNodeCaAndTlsCertificates(nodeCrlDistPoint, tlsCrlDistPoint) + val keyStore = clientConfig.p2pSslOptions.keyStore.get() + + clientAmqpConfig = object : AMQPConfiguration { + override val keyStore = keyStore + override val trustStore = clientConfig.p2pSslOptions.trustStore.get() + override val maxMessageSize: Int = MAX_MESSAGE_SIZE + override val sslHandshakeTimeout: Long = 3000 + } + + clientKeyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()) + clientTrustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) + + clientKeyManagerFactory.init(keyStore) + clientTrustManagerFactory.init(initialiseTrustStoreAndEnableCrlChecking(clientAmqpConfig.trustStore, clientAmqpConfig.revocationConfig)) + } + + @Test(timeout = 300_000) + fun trivialClientServerExchange() { + val serverPort = portAllocation.nextPort() + val serverThread = ServerThread(serverKeyManagerFactory, serverTrustManagerFactory, serverPort).also { it.start() } + + //System.setProperty("javax.net.debug", "all"); + + serverThread.use { + val client = NioSslClient(clientKeyManagerFactory, clientTrustManagerFactory, "localhost", serverPort) + client.connect() + client.write("Hello! I am a client!") + client.read() + client.shutdown() + + val client2 = NioSslClient(clientKeyManagerFactory, clientTrustManagerFactory, "localhost", serverPort) + val client3 = NioSslClient(clientKeyManagerFactory, clientTrustManagerFactory, "localhost", serverPort) + val client4 = NioSslClient(clientKeyManagerFactory, clientTrustManagerFactory, "localhost", serverPort) + + client2.connect() + client2.write("Hello! I am another client!") + client2.read() + client2.shutdown() + + client3.connect() + client4.connect() + client3.write("Hello from client3!!!") + client4.write("Hello from client4!!!") + client3.read() + client4.read() + client3.shutdown() + client4.shutdown() + } + assertFalse(serverThread.isActive) + } + + @Test(timeout = 300_000) + fun amqpClientServerConnect() { + val serverPort = portAllocation.nextPort() + val serverThread = ServerThread(serverKeyManagerFactory, serverTrustManagerFactory, serverPort) + .also { it.start() } + serverThread.use { + val amqpClient = AMQPClient(listOf(NetworkHostAndPort("localhost", serverPort)), setOf(ALICE_NAME), clientAmqpConfig) + + amqpClient.use { + val clientConnected = amqpClient.onConnection.toFuture() + amqpClient.start() + val clientConnect = clientConnected.get() + assertTrue(clientConnect.connected) + + log.info("Confirmed connected") + } + } + assertFalse(serverThread.isActive) + } + + @Test(timeout = 300_000) + fun amqpClientServerHandshakeTimeout() { + val serverPort = portAllocation.nextPort() + val serverThread = ServerThread(serverKeyManagerFactory, serverTrustManagerFactory, serverPort, 5.seconds) + .also { it.start() } + serverThread.use { + val amqpClient = AMQPClient(listOf(NetworkHostAndPort("localhost", serverPort)), setOf(ALICE_NAME), clientAmqpConfig) + + amqpClient.use { + val clientConnected = amqpClient.onConnection.toFuture() + amqpClient.start() + val clientConnect = clientConnected.get() + assertFalse(clientConnect.connected) + // Not a badCert, but a timeout during handshake + assertFalse(clientConnect.badCert) + } + } + assertFalse(serverThread.isActive) + } +} \ No newline at end of file From 39dbe22c9d7fc0845371f63fead83cafcf01f182 Mon Sep 17 00:00:00 2001 From: LankyDan Date: Fri, 31 Jul 2020 12:37:44 +0100 Subject: [PATCH 16/45] ENT-5532 Terminate sessions after original io request Sessions are now terminated after performing the original `FlowIORequest` passed into `StartedFlowTransition`, instead of before. This is done by scheduling an `Event.TerminateSessions` if there are sessions to terminate when performing a suspending event. Originally this was done by hijacking a transition that is trying to perform a `StartedFlowTransition`, terminating the sessions and then scheduling another `Event.DoRemainingWork` to perform the original transition. This introduced a bug where, another event (from a external message) could be placed onto the queue before the `Event.DoRemainingWork` could be added. In most scenarios, that should be ok. But, if a flow is retrying (while in an uninitiated state) and this occurs the flow could fail due to being in an unexpected state. Terminating the sessions after performing the original transition removes this possibility. Meaning that a restarting flow will always perform the transition they supposed to do (based on the called suspending event). --- .../corda/node/services/statemachine/Event.kt | 9 ++- .../transitions/StartedFlowTransition.kt | 74 +++++++++---------- .../transitions/TopLevelTransition.kt | 13 ++++ 3 files changed, 54 insertions(+), 42 deletions(-) diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/Event.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/Event.kt index 3c638584e9..a44a66b14a 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/Event.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/Event.kt @@ -139,7 +139,7 @@ sealed class Event { data class AsyncOperationCompletion(val returnValue: Any?) : Event() /** - * Signals the faiure of a [FlowAsyncOperation]. + * Signals the failure of a [FlowAsyncOperation]. * * Scheduling is triggered by the service that completes the future returned by the async operation. * @@ -179,6 +179,13 @@ sealed class Event { override fun toString() = "WakeUpSleepyFlow" } + /** + * Terminate the specified [sessions], removing them from in-memory datastructures. + * + * @param sessions The sessions to terminate + */ + data class TerminateSessions(val sessions: Set) : Event() + /** * Indicates that an event was generated by an external event and that external event needs to be replayed if we retry the flow, * even if it has not yet been processed and placed on the pending de-duplication handlers list. diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/transitions/StartedFlowTransition.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/transitions/StartedFlowTransition.kt index cea423134f..9b2469face 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/transitions/StartedFlowTransition.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/transitions/StartedFlowTransition.kt @@ -41,47 +41,18 @@ class StartedFlowTransition( continuation = FlowContinuation.Throw(errorsToThrow[0]) ) } - val sessionsToBeTerminated = findSessionsToBeTerminated(startingState) - // if there are sessions to be closed, we close them as part of this transition and normal processing will continue on the next transition. - return if (sessionsToBeTerminated.isNotEmpty()) { - terminateSessions(sessionsToBeTerminated) - } else { - when (flowIORequest) { - is FlowIORequest.Send -> sendTransition(flowIORequest) - is FlowIORequest.Receive -> receiveTransition(flowIORequest) - is FlowIORequest.SendAndReceive -> sendAndReceiveTransition(flowIORequest) - is FlowIORequest.CloseSessions -> closeSessionTransition(flowIORequest) - is FlowIORequest.WaitForLedgerCommit -> waitForLedgerCommitTransition(flowIORequest) - is FlowIORequest.Sleep -> sleepTransition(flowIORequest) - is FlowIORequest.GetFlowInfo -> getFlowInfoTransition(flowIORequest) - is FlowIORequest.WaitForSessionConfirmations -> waitForSessionConfirmationsTransition() - is FlowIORequest.ExecuteAsyncOperation<*> -> executeAsyncOperation(flowIORequest) - FlowIORequest.ForceCheckpoint -> executeForceCheckpoint() - } - } - } - - private fun findSessionsToBeTerminated(startingState: StateMachineState): SessionMap { - return startingState.checkpoint.checkpointState.sessionsToBeClosed.mapNotNull { sessionId -> - val sessionState = startingState.checkpoint.checkpointState.sessions[sessionId]!! as SessionState.Initiated - if (sessionState.receivedMessages.isNotEmpty() && sessionState.receivedMessages.first() is EndSessionMessage) { - sessionId to sessionState - } else { - null - } - }.toMap() - } - - private fun terminateSessions(sessionsToBeTerminated: SessionMap): TransitionResult { - return builder { - val sessionsToRemove = sessionsToBeTerminated.keys - val newCheckpoint = currentState.checkpoint.removeSessions(sessionsToRemove) - .removeSessionsToBeClosed(sessionsToRemove) - currentState = currentState.copy(checkpoint = newCheckpoint) - actions.add(Action.RemoveSessionBindings(sessionsToRemove)) - actions.add(Action.ScheduleEvent(Event.DoRemainingWork)) - FlowContinuation.ProcessEvents - } + return when (flowIORequest) { + is FlowIORequest.Send -> sendTransition(flowIORequest) + is FlowIORequest.Receive -> receiveTransition(flowIORequest) + is FlowIORequest.SendAndReceive -> sendAndReceiveTransition(flowIORequest) + is FlowIORequest.CloseSessions -> closeSessionTransition(flowIORequest) + is FlowIORequest.WaitForLedgerCommit -> waitForLedgerCommitTransition(flowIORequest) + is FlowIORequest.Sleep -> sleepTransition(flowIORequest) + is FlowIORequest.GetFlowInfo -> getFlowInfoTransition(flowIORequest) + is FlowIORequest.WaitForSessionConfirmations -> waitForSessionConfirmationsTransition() + is FlowIORequest.ExecuteAsyncOperation<*> -> executeAsyncOperation(flowIORequest) + FlowIORequest.ForceCheckpoint -> executeForceCheckpoint() + }.let { scheduleTerminateSessionsIfRequired(it) } } private fun waitForSessionConfirmationsTransition(): TransitionResult { @@ -537,4 +508,25 @@ class StartedFlowTransition( private fun executeForceCheckpoint(): TransitionResult { return builder { resumeFlowLogic(Unit) } } + + private fun scheduleTerminateSessionsIfRequired(transition: TransitionResult): TransitionResult { + // If there are sessions to be closed, close them on a following transition + val sessionsToBeTerminated = findSessionsToBeTerminated(transition.newState) + return if (sessionsToBeTerminated.isNotEmpty()) { + transition.copy(actions = transition.actions + Action.ScheduleEvent(Event.TerminateSessions(sessionsToBeTerminated.keys))) + } else { + transition + } + } + + private fun findSessionsToBeTerminated(startingState: StateMachineState): SessionMap { + return startingState.checkpoint.checkpointState.sessionsToBeClosed.mapNotNull { sessionId -> + val sessionState = startingState.checkpoint.checkpointState.sessions[sessionId]!! as SessionState.Initiated + if (sessionState.receivedMessages.isNotEmpty() && sessionState.receivedMessages.first() is EndSessionMessage) { + sessionId to sessionState + } else { + null + } + }.toMap() + } } diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/transitions/TopLevelTransition.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/transitions/TopLevelTransition.kt index 169148108e..998625105f 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/transitions/TopLevelTransition.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/transitions/TopLevelTransition.kt @@ -62,6 +62,7 @@ class TopLevelTransition( is Event.ReloadFlowFromCheckpointAfterSuspend -> reloadFlowFromCheckpointAfterSuspendTransition() is Event.OvernightObservation -> overnightObservationTransition() is Event.WakeUpFromSleep -> wakeUpFromSleepTransition() + is Event.TerminateSessions -> terminateSessionsTransition(event) } } @@ -366,4 +367,16 @@ class TopLevelTransition( resumeFlowLogic(Unit) } } + + private fun terminateSessionsTransition(event: Event.TerminateSessions): TransitionResult { + return builder { + val sessions = event.sessions + val newCheckpoint = currentState.checkpoint + .removeSessions(sessions) + .removeSessionsToBeClosed(sessions) + currentState = currentState.copy(checkpoint = newCheckpoint) + actions.add(Action.RemoveSessionBindings(sessions)) + FlowContinuation.ProcessEvents + } + } } From 82bcde573b15650e477bb7df822288da7e985c8e Mon Sep 17 00:00:00 2001 From: LankyDan Date: Fri, 31 Jul 2020 14:28:57 +0100 Subject: [PATCH 17/45] NOTICK Resume flow when wrong message received When an incorrect message is received, the flow should resume to allow it to throw the error back to user code and possibly cause the flow to fail. For now, if an `EndSessionMessage` is received instead of a `DataSessionMessage`, then an `UnexpectedFlowEndException` is thrown back to user code. Allowing it to correctly re-enter normal flow error handling. Without this change, the flow will hang due to it failing while creating a transition which exists outside of the general state machine error handling code path. --- .../transitions/StartedFlowTransition.kt | 50 +++++++++++++------ .../statemachine/RetryFlowMockTest.kt | 2 +- 2 files changed, 35 insertions(+), 17 deletions(-) diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/transitions/StartedFlowTransition.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/transitions/StartedFlowTransition.kt index 9b2469face..0911ed18a4 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/transitions/StartedFlowTransition.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/transitions/StartedFlowTransition.kt @@ -129,6 +129,7 @@ class StartedFlowTransition( } } + @Suppress("TooGenericExceptionCaught") private fun sendAndReceiveTransition(flowIORequest: FlowIORequest.SendAndReceive): TransitionResult { val sessionIdToMessage = LinkedHashMap>() val sessionIdToSession = LinkedHashMap() @@ -142,18 +143,23 @@ class StartedFlowTransition( if (isErrored()) { FlowContinuation.ProcessEvents } else { - val receivedMap = receiveFromSessionsTransition(sessionIdToSession) - if (receivedMap == null) { - // We don't yet have the messages, change the suspension to be on Receive - val newIoRequest = FlowIORequest.Receive(flowIORequest.sessionToMessage.keys.toNonEmptySet()) - currentState = currentState.copy( + try { + val receivedMap = receiveFromSessionsTransition(sessionIdToSession) + if (receivedMap == null) { + // We don't yet have the messages, change the suspension to be on Receive + val newIoRequest = FlowIORequest.Receive(flowIORequest.sessionToMessage.keys.toNonEmptySet()) + currentState = currentState.copy( checkpoint = currentState.checkpoint.copy( - flowState = FlowState.Started(newIoRequest, started.frozenFiber) + flowState = FlowState.Started(newIoRequest, started.frozenFiber) ) - ) - FlowContinuation.ProcessEvents - } else { - resumeFlowLogic(receivedMap) + ) + FlowContinuation.ProcessEvents + } else { + resumeFlowLogic(receivedMap) + } + } catch (t: Throwable) { + // E.g. A session end message received while expecting a data session message + resumeFlowLogic(t) } } } @@ -187,6 +193,7 @@ class StartedFlowTransition( } } + @Suppress("TooGenericExceptionCaught") private fun receiveTransition(flowIORequest: FlowIORequest.Receive): TransitionResult { return builder { val sessionIdToSession = LinkedHashMap() @@ -195,11 +202,16 @@ class StartedFlowTransition( } // send initialises to uninitialised sessions sendInitialSessionMessagesIfNeeded(sessionIdToSession.keys) - val receivedMap = receiveFromSessionsTransition(sessionIdToSession) - if (receivedMap == null) { - FlowContinuation.ProcessEvents - } else { - resumeFlowLogic(receivedMap) + try { + val receivedMap = receiveFromSessionsTransition(sessionIdToSession) + if (receivedMap == null) { + FlowContinuation.ProcessEvents + } else { + resumeFlowLogic(receivedMap) + } + } catch (t: Throwable) { + // E.g. A session end message received while expecting a data session message + resumeFlowLogic(t) } } } @@ -224,6 +236,8 @@ class StartedFlowTransition( val messages: Map>, val newSessionMap: SessionMap ) + + @Suppress("ComplexMethod", "NestedBlockDepth") private fun pollSessionMessages(sessions: SessionMap, sessionIds: Set): PollResult? { val newSessionMessages = LinkedHashMap(sessions) val resultMessages = LinkedHashMap>() @@ -238,7 +252,11 @@ class StartedFlowTransition( } else { newSessionMessages[sessionId] = sessionState.copy(receivedMessages = messages.subList(1, messages.size).toList()) // at this point, we've already checked for errors and session ends, so it's guaranteed that the first message will be a data message. - resultMessages[sessionId] = (messages[0] as DataSessionMessage).payload + resultMessages[sessionId] = if (messages[0] is EndSessionMessage) { + throw UnexpectedFlowEndException("Received session end message instead of a data session message. Mismatched send and receive?") + } else { + (messages[0] as DataSessionMessage).payload + } } } else -> { diff --git a/node/src/test/kotlin/net/corda/node/services/statemachine/RetryFlowMockTest.kt b/node/src/test/kotlin/net/corda/node/services/statemachine/RetryFlowMockTest.kt index ee93d937d2..061345efe7 100644 --- a/node/src/test/kotlin/net/corda/node/services/statemachine/RetryFlowMockTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/statemachine/RetryFlowMockTest.kt @@ -97,7 +97,7 @@ class RetryFlowMockTest { } @Test(timeout=300_000) - fun `Restart does not set senderUUID`() { + fun `Restart does not set senderUUID and early end session message does not hang receiving flow`() { val messagesSent = Collections.synchronizedList(mutableListOf()) val partyB = nodeB.info.legalIdentities.first() nodeA.setMessagingServiceSpy(object : MessagingServiceSpy() { From 9aa745ef14f7625c159d9cc884868c50df250aea Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Fri, 31 Jul 2020 15:54:53 +0100 Subject: [PATCH 18/45] INFRA-553 Move HashLookupCommandTest to unit test (#6537) Move HashLookupCommandTest to unit test and add a test covering that a nonsense hash ID is correctly rejected. --- .../tools/shell/HashLookupCommandTest.kt | 96 ------------------- .../tools/shell/HashLookupShellCommand.java | 27 ++++-- .../tools/shell/HashLookupCommandTest.kt | 67 +++++++++++++ 3 files changed, 85 insertions(+), 105 deletions(-) delete mode 100644 tools/shell/src/integration-test/kotlin/net/corda/tools/shell/HashLookupCommandTest.kt create mode 100644 tools/shell/src/test/kotlin/net/corda/tools/shell/HashLookupCommandTest.kt diff --git a/tools/shell/src/integration-test/kotlin/net/corda/tools/shell/HashLookupCommandTest.kt b/tools/shell/src/integration-test/kotlin/net/corda/tools/shell/HashLookupCommandTest.kt deleted file mode 100644 index 2376081a4a..0000000000 --- a/tools/shell/src/integration-test/kotlin/net/corda/tools/shell/HashLookupCommandTest.kt +++ /dev/null @@ -1,96 +0,0 @@ -package net.corda.tools.shell - -import co.paralleluniverse.fibers.Suspendable -import com.google.common.io.Files -import com.jcraft.jsch.ChannelExec -import com.jcraft.jsch.JSch -import com.jcraft.jsch.Session -import net.corda.core.crypto.SecureHash -import net.corda.core.crypto.sha256 -import net.corda.core.flows.FlowLogic -import net.corda.core.flows.StartableByRPC -import net.corda.core.identity.Party -import net.corda.core.messaging.startFlow -import net.corda.core.utilities.getOrThrow -import net.corda.node.services.Permissions -import net.corda.testing.contracts.DummyContract -import net.corda.testing.core.ALICE_NAME -import net.corda.testing.driver.DriverParameters -import net.corda.testing.driver.NodeHandle -import net.corda.testing.driver.driver -import net.corda.testing.node.User -import net.corda.testing.node.internal.DUMMY_CONTRACTS_CORDAPP -import org.bouncycastle.util.io.Streams -import org.junit.Test -import kotlin.test.assertTrue - -class HashLookupCommandTest { - - @Test(timeout=300_000) - fun `hash lookup command returns correct response`() { - val user = User("u", "p", setOf(Permissions.all())) - - driver(DriverParameters(notarySpecs = emptyList(), cordappsForAllNodes = listOf(DUMMY_CONTRACTS_CORDAPP))) { - val nodeFuture = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user), startInSameProcess = true) - val node = nodeFuture.getOrThrow() - val txId = issueTransaction(node) - val txIdHashed = txId.sha256() - - testCommand(user, node, command = "hashLookup $txId", expected = "Found a matching transaction with Id: $txId") - testCommand(user, node, command = "hashLookup $txIdHashed", expected = "Found a matching transaction with Id: $txId") - testCommand(user, node, command = "hashLookup ${SecureHash.randomSHA256()}", expected = "No matching transaction found") - } - } - - private fun testCommand(user: User, node: NodeHandle, command: String, expected: String) { - val session = connectToShell(user, node) - val channel = session.openChannel("exec") as ChannelExec - channel.setCommand(command) - channel.connect(5000) - - assertTrue(channel.isConnected) - - val response = String(Streams.readAll(channel.inputStream)) - val matchFound = response.lines().any { line -> - line.contains(expected) - } - channel.disconnect() - assertTrue(matchFound) - session.disconnect() - } - - private fun connectToShell(user: User, node: NodeHandle): Session { - val conf = ShellConfiguration(commandsDirectory = Files.createTempDir().toPath(), - user = user.username, password = user.password, - hostAndPort = node.rpcAddress, - sshdPort = 2224) - - InteractiveShell.startShell(conf) - InteractiveShell.nodeInfo() - - val session = JSch().getSession("u", "localhost", 2224) - session.setConfig("StrictHostKeyChecking", "no") - session.setPassword("p") - session.connect() - - assertTrue(session.isConnected) - return session - } - - private fun issueTransaction(node: NodeHandle): SecureHash { - return node.rpc.startFlow(::DummyIssue).returnValue.get() - } -} - -@StartableByRPC -internal class DummyIssue : FlowLogic() { - @Suspendable - override fun call(): SecureHash { - val me = serviceHub.myInfo.legalIdentities.first().ref(0) - val fakeNotary = me.party - val builder = DummyContract.generateInitial(1, fakeNotary as Party, me) - val stx = serviceHub.signInitialTransaction(builder) - serviceHub.recordTransactions(stx) - return stx.id - } -} diff --git a/tools/shell/src/main/java/net/corda/tools/shell/HashLookupShellCommand.java b/tools/shell/src/main/java/net/corda/tools/shell/HashLookupShellCommand.java index 3fcd803d8e..7d09802088 100644 --- a/tools/shell/src/main/java/net/corda/tools/shell/HashLookupShellCommand.java +++ b/tools/shell/src/main/java/net/corda/tools/shell/HashLookupShellCommand.java @@ -1,6 +1,7 @@ package net.corda.tools.shell; import net.corda.core.crypto.SecureHash; +import net.corda.core.internal.VisibleForTesting; import net.corda.core.messaging.CordaRPCOps; import net.corda.core.messaging.StateMachineTransactionMapping; import org.crsh.cli.Argument; @@ -13,13 +14,14 @@ import org.crsh.text.Decoration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.PrintWriter; import java.util.List; import java.util.Optional; @Named("hashLookup") public class HashLookupShellCommand extends InteractiveShellCommand { private static Logger logger = LoggerFactory.getLogger(HashLookupShellCommand.class); - final private String manualText ="Checks if a transaction matching a specified Id hash value is recorded on this node.\n\n" + + private static final String manualText ="Checks if a transaction matching a specified Id hash value is recorded on this node.\n\n" + "Both the transaction Id and the hashed value of a transaction Id (as returned by the Notary in case of a double-spend) is a valid input.\n" + "This is mainly intended to be used for troubleshooting notarisation issues when a\n" + "state is claimed to be already consumed by another transaction.\n\n" + @@ -29,25 +31,32 @@ public class HashLookupShellCommand extends InteractiveShellCommand { @Man(manualText) public void main(@Usage("A transaction Id or a hexadecimal SHA-256 hash value representing the hashed transaction Id") @Argument(unquote = false) String txIdHash) { + CordaRPCOps proxy = ops(); + try { + hashLookup(out, proxy, txIdHash); + } catch (IllegalArgumentException ex) { + out.println(manualText); + out.println(ex.getMessage(), Decoration.bold, Color.red); + } + } + + @VisibleForTesting + protected static void hashLookup(PrintWriter out, CordaRPCOps proxy, String txIdHash) throws IllegalArgumentException { logger.info("Executing command \"hashLookup\"."); if (txIdHash == null) { out.println(manualText); - out.println("Please provide a hexadecimal transaction Id hash value or a transaction Id", Decoration.bold, Color.red); - return; + throw new IllegalArgumentException("Please provide a hexadecimal transaction Id hash value or a transaction Id"); } - CordaRPCOps proxy = ops(); - List mapping = proxy.stateMachineRecordedTransactionMappingSnapshot(); - SecureHash txIdHashParsed; try { txIdHashParsed = SecureHash.parse(txIdHash); } catch (IllegalArgumentException e) { - out.println("The provided string is not a valid hexadecimal SHA-256 hash value", Decoration.bold, Color.red); - return; + throw new IllegalArgumentException("The provided string is not a valid hexadecimal SHA-256 hash value"); } + List mapping = proxy.stateMachineRecordedTransactionMappingSnapshot(); Optional match = mapping.stream() .map(StateMachineTransactionMapping::getTransactionId) .filter( @@ -59,7 +68,7 @@ public class HashLookupShellCommand extends InteractiveShellCommand { SecureHash found = match.get(); out.println("Found a matching transaction with Id: " + found.toString()); } else { - out.println("No matching transaction found", Decoration.bold, Color.red); + throw new IllegalArgumentException("No matching transaction found"); } } } diff --git a/tools/shell/src/test/kotlin/net/corda/tools/shell/HashLookupCommandTest.kt b/tools/shell/src/test/kotlin/net/corda/tools/shell/HashLookupCommandTest.kt new file mode 100644 index 0000000000..15b4e951d8 --- /dev/null +++ b/tools/shell/src/test/kotlin/net/corda/tools/shell/HashLookupCommandTest.kt @@ -0,0 +1,67 @@ +package net.corda.tools.shell + +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.sha256 +import net.corda.core.flows.StateMachineRunId +import net.corda.core.messaging.CordaRPCOps +import net.corda.core.messaging.StateMachineTransactionMapping +import org.hamcrest.MatcherAssert +import org.hamcrest.core.StringContains +import org.junit.Test +import org.mockito.Mockito +import java.io.CharArrayWriter +import java.io.PrintWriter +import java.util.UUID +import kotlin.test.assertFailsWith + +class HashLookupCommandTest { + companion object { + private val DEFAULT_TXID: SecureHash = SecureHash.randomSHA256() + + private fun ops(vararg txIds: SecureHash): CordaRPCOps? { + val snapshot: List = txIds.map { txId -> + StateMachineTransactionMapping(StateMachineRunId(UUID.randomUUID()), txId) + } + return Mockito.mock(CordaRPCOps::class.java).apply { + Mockito.`when`(stateMachineRecordedTransactionMappingSnapshot()).thenReturn(snapshot) + } + } + + private fun runCommand(ops: CordaRPCOps?, txIdHash: String): String { + val arrayWriter = CharArrayWriter() + return PrintWriter(arrayWriter).use { + HashLookupShellCommand.hashLookup(it, ops, txIdHash) + it.flush() + arrayWriter.toString() + } + } + } + + @Test(timeout=300_000) + fun `hash lookup command returns correct response`() { + val ops = ops(DEFAULT_TXID) + var response = runCommand(ops, DEFAULT_TXID.toString()) + + MatcherAssert.assertThat(response, StringContains.containsString("Found a matching transaction with Id: $DEFAULT_TXID")) + + // Verify the hash of the TX ID also works + response = runCommand(ops, DEFAULT_TXID.sha256().toString()) + MatcherAssert.assertThat(response, StringContains.containsString("Found a matching transaction with Id: $DEFAULT_TXID")) + } + + @Test(timeout=300_000) + fun `should reject invalid txid`() { + val ops = ops(DEFAULT_TXID) + assertFailsWith("The provided string is not a valid hexadecimal SHA-256 hash value") { + runCommand(ops, "abcdefgh") + } + } + + @Test(timeout=300_000) + fun `should reject unknown txid`() { + val ops = ops(DEFAULT_TXID) + assertFailsWith("No matching transaction found") { + runCommand(ops, SecureHash.randomSHA256().toString()) + } + } +} \ No newline at end of file From 85be50779b4709e02e65646ee0382347c9444a33 Mon Sep 17 00:00:00 2001 From: Tamas Veingartner Date: Mon, 3 Aug 2020 09:19:48 +0100 Subject: [PATCH 19/45] =?UTF-8?q?CORDA-3663=20MockServices=20crashes=20whe?= =?UTF-8?q?n=20two=20of=20the=20provided=20packages=20to=20=E2=80=A6=20(#6?= =?UTF-8?q?472)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * CORDA-3663 MockServices crashes when two of the provided packages to scan are deemed empty in 4.4 RC05 this happends when a given package is not found on the classpath. Now it is handled and an exception is thrown * replace dummy package names in tests with valid ones * allow empty package list for CustomCordapps and exclude those from the created jars * detekt fix * always true logic fix * fix to check for empty packages instead of empty classes * fix for classes and fixups * logic refactor because of detekt stupidity * PR related minor refactors --- .../coretests/flows/FinalityFlowTests.kt | 2 +- .../corda/node/internal/CordaServiceTest.kt | 26 +++++++++++++++++++ .../testing/node/internal/CustomCordapp.kt | 15 +++++------ .../node/internal/TestCordappInternal.kt | 7 +++-- .../internal/TestResponseFlowInIsolation.kt | 2 +- .../TestResponseFlowInIsolationInJava.java | 2 +- 6 files changed, 41 insertions(+), 13 deletions(-) diff --git a/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt b/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt index 288f931c1e..1d13b53c66 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt @@ -96,5 +96,5 @@ class FinalityFlowTests : WithFinality { } /** "Old" CorDapp which will force its node to keep its FinalityHandler enabled */ - private fun tokenOldCordapp() = cordappWithPackages("com.template").copy(targetPlatformVersion = 3) + private fun tokenOldCordapp() = cordappWithPackages().copy(targetPlatformVersion = 3) } diff --git a/node/src/test/kotlin/net/corda/node/internal/CordaServiceTest.kt b/node/src/test/kotlin/net/corda/node/internal/CordaServiceTest.kt index 8e76c1ef5c..1d06c470c8 100644 --- a/node/src/test/kotlin/net/corda/node/internal/CordaServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/internal/CordaServiceTest.kt @@ -6,6 +6,8 @@ import net.corda.core.context.InvocationOrigin import net.corda.core.contracts.ContractState import net.corda.core.flows.FlowLogic import net.corda.core.flows.StartableByService +import net.corda.core.identity.CordaX500Name +import net.corda.core.internal.packageName import net.corda.core.node.AppServiceHub import net.corda.core.node.ServiceHub import net.corda.core.node.services.CordaService @@ -16,12 +18,20 @@ import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.ProgressTracker import net.corda.finance.DOLLARS import net.corda.finance.flows.CashIssueFlow +import net.corda.finance.schemas.CashSchemaV1 import net.corda.node.internal.cordapp.DummyRPCFlow +import net.corda.testing.core.BOC_NAME +import net.corda.testing.core.DUMMY_NOTARY_NAME +import net.corda.testing.core.TestIdentity +import net.corda.testing.internal.vault.DummyLinearStateSchemaV1 import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNetworkParameters +import net.corda.testing.node.MockServices import net.corda.testing.node.StartedMockNode import net.corda.testing.node.internal.FINANCE_CONTRACTS_CORDAPP import net.corda.testing.node.internal.enclosedCordapp +import net.corda.testing.node.makeTestIdentityService +import org.assertj.core.api.Assertions import org.junit.After import org.junit.Before import org.junit.Test @@ -100,6 +110,22 @@ class CordaServiceTest { nodeA.services.cordaService(EntityManagerService::class.java) } + @Test(timeout=300_000) + fun `MockServices when initialized with package name not on classpath throws ClassNotFoundException`() { + val cordappPackages = listOf( + "com.r3.corda.sdk.tokens.money", + "net.corda.finance.contracts", + CashSchemaV1::class.packageName, + DummyLinearStateSchemaV1::class.packageName) + val bankOfCorda = TestIdentity(BOC_NAME) + val dummyCashIssuer = TestIdentity(CordaX500Name("Snake Oil Issuer", "London", "GB"), 10) + val dummyNotary = TestIdentity(DUMMY_NOTARY_NAME, 20) + val identityService = makeTestIdentityService(dummyNotary.identity) + + Assertions.assertThatThrownBy { MockServices(cordappPackages, dummyNotary, identityService, dummyCashIssuer.keyPair, bankOfCorda.keyPair) } + .isInstanceOf(ClassNotFoundException::class.java).hasMessage("Could not create jar file as the given package is not found on the classpath: com.r3.corda.sdk.tokens.money") + } + @StartableByService class DummyServiceFlow : FlowLogic() { companion object { diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/CustomCordapp.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/CustomCordapp.kt index 9d59807e95..37590387b3 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/CustomCordapp.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/CustomCordapp.kt @@ -37,11 +37,6 @@ data class CustomCordapp( val signingInfo: SigningInfo? = null, override val config: Map = emptyMap() ) : TestCordappInternal() { - init { - require(packages.isNotEmpty() || classes.isNotEmpty() || fixups.isNotEmpty()) { - "At least one package or class must be specified" - } - } override val jarFile: Path get() = getJarFile(this) @@ -55,7 +50,7 @@ data class CustomCordapp( @VisibleForTesting internal fun packageAsJar(file: Path) { val classGraph = ClassGraph() - if (packages.isNotEmpty()) { + if(packages.isNotEmpty()){ classGraph.whitelistPaths(*packages.map { it.replace('.', '/') }.toTypedArray()) } if (classes.isNotEmpty()) { @@ -78,6 +73,10 @@ data class CustomCordapp( } } + if (scanResult.allResources.isEmpty()){ + throw ClassNotFoundException("Could not create jar file as the given package is not found on the classpath: ${packages.toList()[0]}") + } + // The same resource may be found in different locations (this will happen when running from gradle) so just // pick the first one found. scanResult.allResources.asMap().forEach { path, resourceList -> @@ -178,8 +177,8 @@ data class CustomCordapp( val jarFile = cordappsDirectory.createDirectories() / filename if (it.fixups.isNotEmpty()) { it.createFixupJar(jarFile) - } else { - it.packageAsJar(jarFile) + } else if(it.packages.isNotEmpty() || it.classes.isNotEmpty() || it.fixups.isNotEmpty()) { + it.packageAsJar(jarFile) } it.signJar(jarFile) logger.debug { "$it packaged into $jarFile" } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/TestCordappInternal.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/TestCordappInternal.kt index 3380c37b5d..d04eb9f147 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/TestCordappInternal.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/TestCordappInternal.kt @@ -30,14 +30,17 @@ abstract class TestCordappInternal : TestCordapp() { // Precedence is given to node-specific CorDapps val allCordapps = nodeSpecificCordapps + generalCordapps.filter { it.withOnlyJarContents() !in nodeSpecificCordappsWithoutMeta } // Ignore any duplicate jar files - val jarToCordapp = allCordapps.associateBy { it.jarFile } + val jarToCordapp = allCordapps.filter { + it !is CustomCordapp || it.packages.isNotEmpty() || it.classes.isNotEmpty() || it.fixups.isNotEmpty() }.associateBy { it.jarFile } val cordappsDir = baseDirectory / "cordapps" val configDir = (cordappsDir / "config").createDirectories() jarToCordapp.forEach { jar, cordapp -> try { - jar.copyToDirectory(cordappsDir) + if (jar.toFile().exists()) { + jar.copyToDirectory(cordappsDir) + } } catch (e: FileAlreadyExistsException) { // Ignore if the node already has the same CorDapp jar. This can happen if the node is being restarted. } diff --git a/testing/node-driver/src/test/kotlin/net/corda/testing/node/internal/TestResponseFlowInIsolation.kt b/testing/node-driver/src/test/kotlin/net/corda/testing/node/internal/TestResponseFlowInIsolation.kt index ce96875ef7..9806c56e11 100644 --- a/testing/node-driver/src/test/kotlin/net/corda/testing/node/internal/TestResponseFlowInIsolation.kt +++ b/testing/node-driver/src/test/kotlin/net/corda/testing/node/internal/TestResponseFlowInIsolation.kt @@ -22,7 +22,7 @@ import org.junit.Test */ class TestResponseFlowInIsolation { - private val network: MockNetwork = MockNetwork(MockNetworkParameters(cordappsForAllNodes = cordappsForPackages("com.template"))) + private val network: MockNetwork = MockNetwork(MockNetworkParameters(cordappsForAllNodes = cordappsForPackages())) private val a = network.createNode() private val b = network.createNode() diff --git a/testing/node-driver/src/test/kotlin/net/corda/testing/node/internal/TestResponseFlowInIsolationInJava.java b/testing/node-driver/src/test/kotlin/net/corda/testing/node/internal/TestResponseFlowInIsolationInJava.java index 8aead69f18..14947872ec 100644 --- a/testing/node-driver/src/test/kotlin/net/corda/testing/node/internal/TestResponseFlowInIsolationInJava.java +++ b/testing/node-driver/src/test/kotlin/net/corda/testing/node/internal/TestResponseFlowInIsolationInJava.java @@ -28,7 +28,7 @@ import static org.hamcrest.Matchers.instanceOf; */ public class TestResponseFlowInIsolationInJava { - private final MockNetwork network = new MockNetwork(new MockNetworkParameters().withCordappsForAllNodes(cordappsForPackages("com.template"))); + private final MockNetwork network = new MockNetwork(new MockNetworkParameters().withCordappsForAllNodes(cordappsForPackages())); private final StartedMockNode a = network.createNode(); private final StartedMockNode b = network.createNode(); From 9f2bd94f1bffc4665afad28e3a97ec0266272cd2 Mon Sep 17 00:00:00 2001 From: Christian Sailer Date: Mon, 3 Aug 2020 10:30:14 +0100 Subject: [PATCH 20/45] ENT-5624 Remove code to migrate from pre-V4 versions of Corda - these will have to go via Corda 4.5 (#6548) --- .../internal/persistence/SchemaMigration.kt | 61 ------------------- 1 file changed, 61 deletions(-) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/SchemaMigration.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/SchemaMigration.kt index bbb9ad456a..b528203c00 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/SchemaMigration.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/SchemaMigration.kt @@ -54,7 +54,6 @@ open class SchemaMigration( * when allowing hibernate to create missing schemas in dev or tests. */ fun runMigration(existingCheckpoints: Boolean, schemas: Set, forceThrowOnMissingMigration: Boolean) { - migrateOlderDatabaseToUseLiquibase(existingCheckpoints, schemas) val resourcesAndSourceInfo = prepareResources(schemas, forceThrowOnMissingMigration) // current version of Liquibase appears to be non-threadsafe @@ -180,66 +179,6 @@ open class SchemaMigration( return Triple(liquibase, unRunChanges.size, !unRunChanges.isEmpty()) } - /** For existing database created before verions 4.0 add Liquibase support - creates DATABASECHANGELOG and DATABASECHANGELOGLOCK tables and marks changesets as executed. */ - private fun migrateOlderDatabaseToUseLiquibase(existingCheckpoints: Boolean, schemas: Set): Boolean { - val isExistingDBWithoutLiquibase = dataSource.connection.use { - - val existingDatabase = it.metaData.getTables(null, null, "NODE%", null).next() - // Lower case names for PostgreSQL - || it.metaData.getTables(null, null, "node%", null).next() - - val hasLiquibase = it.metaData.getTables(null, null, "DATABASECHANGELOG%", null).next() - // Lower case names for PostgreSQL - || it.metaData.getTables(null, null, "databasechangelog%", null).next() - - existingDatabase && !hasLiquibase - } - - if (isExistingDBWithoutLiquibase && existingCheckpoints) - throw CheckpointsException() - - // Schema migrations pre release 4.0 - val preV4Baseline = mutableListOf() - if (isExistingDBWithoutLiquibase) { - preV4Baseline.addAll(listOf("migration/common.changelog-init.xml", - "migration/node-info.changelog-init.xml", - "migration/node-info.changelog-v1.xml", - "migration/node-info.changelog-v2.xml", - "migration/node-core.changelog-init.xml", - "migration/node-core.changelog-v3.xml", - "migration/node-core.changelog-v4.xml", - "migration/node-core.changelog-v5.xml", - "migration/node-core.changelog-pkey.xml", - "migration/vault-schema.changelog-init.xml", - "migration/vault-schema.changelog-v3.xml", - "migration/vault-schema.changelog-v4.xml", - "migration/vault-schema.changelog-pkey.xml")) - - if (schemas.any { schema -> schema.migrationResource == "node-notary.changelog-master" }) - preV4Baseline.addAll(listOf("migration/node-notary.changelog-init.xml", - "migration/node-notary.changelog-v1.xml")) - - if (schemas.any { schema -> schema.migrationResource == "notary-raft.changelog-master" }) - preV4Baseline.addAll(listOf("migration/notary-raft.changelog-init.xml", - "migration/notary-raft.changelog-v1.xml")) - - if (schemas.any { schema -> schema.migrationResource == "notary-bft-smart.changelog-master" }) - preV4Baseline.addAll(listOf("migration/notary-bft-smart.changelog-init.xml", - "migration/notary-bft-smart.changelog-v1.xml")) - } - - if (preV4Baseline.isNotEmpty()) { - val dynamicInclude = "master.changelog.json" // Virtual file name of the changelog that includes all schemas. - checkResourcesInClassPath(preV4Baseline) - dataSource.connection.use { connection -> - val customResourceAccessor = CustomResourceAccessor(dynamicInclude, preV4Baseline, classLoader) - val liquibase = Liquibase(dynamicInclude, customResourceAccessor, databaseFactory.getLiquibaseDatabase(JdbcConnection(connection))) - liquibase.changeLogSync(Contexts(), LabelExpression()) - } - } - return isExistingDBWithoutLiquibase - } - private fun checkResourcesInClassPath(resources: List) { for (resource in resources) { if (resource != null && classLoader.getResource(resource) == null) { From 09b5e21d97e0811a965066f6bc51bd5ea548db42 Mon Sep 17 00:00:00 2001 From: Christian Sailer Date: Mon, 3 Aug 2020 16:43:40 +0100 Subject: [PATCH 21/45] Fix wrong name in test names. --- .../test/kotlin/net/corda/coretests/flows/FlowIsKilledTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core-tests/src/test/kotlin/net/corda/coretests/flows/FlowIsKilledTest.kt b/core-tests/src/test/kotlin/net/corda/coretests/flows/FlowIsKilledTest.kt index fb5e3bc34a..35a1639714 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/flows/FlowIsKilledTest.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/flows/FlowIsKilledTest.kt @@ -116,7 +116,7 @@ class FlowIsKilledTest { } @Test(timeout = 300_000) - fun `manually handle killed flows using checkForIsNotKilled`() { + fun `manually handle killed flows using checkFlowIsNotKilled`() { driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { val alice = startNode(providedName = ALICE_NAME).getOrThrow() alice.rpc.let { rpc -> @@ -135,7 +135,7 @@ class FlowIsKilledTest { } @Test(timeout = 300_000) - fun `manually handle killed flows using checkForIsNotKilled with lazy message`() { + fun `manually handle killed flows using checkFlowIsNotKilled with lazy message`() { driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { val alice = startNode(providedName = ALICE_NAME).getOrThrow() alice.rpc.let { rpc -> From 71a6081ec817fd32137c07b5914a51005d4848f3 Mon Sep 17 00:00:00 2001 From: Christian Sailer Date: Mon, 3 Aug 2020 18:50:36 +0100 Subject: [PATCH 22/45] Fix new integration tests to not use hibernate schema modification. --- lib/quasar.jar | Bin 1265274 -> 1283239 bytes .../node/services/network/NetworkMapTest.kt | 12 ++++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/quasar.jar b/lib/quasar.jar index 789576ab936a6f09d5ac1dde3832c6dc3c08a15d..6f1e8c2fca8dd348735883bda2b326d8b0da3c6e 100644 GIT binary patch delta 538160 zcmZ6RQ*_++u#C2)Hh}QYVqhyVDm4&qykJ65OMVuCd9e@L zXzA9W@coLkM@~_$tgfokU>iSQ$(@^;y$5#Y&QpJYdu?$8dX8-!5su&FOO*z1 zEg@Xkd97f)i4<<(_}H3g^YfyV;Nzc#BT+Y+g8JWsCQ<|Af&J&CiE_a7{}rit0x-m> z`ntfs5I}+ccX3OmDzFLge^<4fB7kUt{Ql3*NmRK;C@>(Psb8smW>A>_*$M{`GB+`@ zlCX7hc66~Zv2`{vrZuuQaB@mknNY+ML*a>Lwqypg3ux&`p#3xpqAi&63@T2l^K%dmi6_#4Gzz(v?%Vx2HWBgw?tL-aTzm2$AYT~nv6 zaH0}qTC5I?)6xa$yfy%zF8-C~R76++Q0;v?%89iqa^c%WNo06j%H zm4Y4;LpO+M+Uosoh>Zxb^vhkzlu_GbN0hw))~wxiAn&x~`IBQP1}F=_#xg=+KQQ8R z6g7xVft9j1t;oH^LZM@Xe&smf!H6VTY{dXeSR>fChHiKoTks1iq^`q=9rkVfM=b~; z*m}lLyl9@RZj#=dUF?BO4&s|W1g5nNDNAZlbcGWekN%ct>Tz)W&w%*(p2U!0YXrQC zcAuE?vpQ6a73|kWf3+t-#^7Rp$i5lW35X=*LcAPZEMxDT!rQsoB;^lN7RQ8TYld}# zixxXdgn{6-B{mzzzVc^x-gt9aEu>qi0vTAU;@M3Mf89`ugiRXEevk=L%i$wM2u$gr z!y(3W7uBQ!zHIq>zmg6!Nqe-J%$_&_XT(p(PppI35Nrfk8`mU2!E`jIUuwxIqrZZV zyZ8Ya8|{24?6q$}TFh1AWiNSuXDI7(+za;LMo(*l8GOVCD#85f!g3>Qc_^ia^SB^z4!_o#g0&r{K^6y?F{7_ zOFO3*y&wIW0PLK5w&+gW6+xw`?|?n&2Q*R7tQw@=td}44D|-JYW`Q@hUV!~ORA}|Qj@aQN8+-PRZKk2K2h{J+G0$2iq*5NWt3kjH_Jd$qkk%{ca z_1Qi<`2UFCgKI%>36ROVo%&?8<;q6{e7&{h1@{Tcqi%LBnlMHZt&}PrY28~dmnYsQ zn_PhJnVWB*Ja8otxL`+st#r$i;uyIK^+Tfpm#FP1cOT1&9y&ura)NKGu7tIQ9sX;owau>9)Nx=NPG*P$rd};)eIS;pVv5nZ7 zU?+wIZ)6k@>@KX2Vd8U&ElDThEES+F2P(t5yRd9t4%^{ zidA^DaJXI9s)yM;`dVpRkx@Lgsv1~rj_B?nL1ESLYjz7@<-5*!1-;tT-S&;qXuJSp z>V>PD;LeAe>fvj+SI(v;4QjJO9!6QZnv9hSQ2?LE6T^Cm^7^G1_s}!5$0OwASrW7c zCc%B=UqB=P>$toWrLy@sTZ8<23(dU~x-kW^nddccu_M)WoJ4jt#SJ;TH^s}A8xuR( zXz%SO^1Kf424^>G4QO$x(1TsVdyTF1iF#%OoDR{9Y1~>eC zN~i|NHGRuS`E^3H1p5dFcem=|3fC4=ZEHHeKLMhJ7WTidBh4SN!&@{XH@HTQ8rHc)db^zWEPLzRN+FJlHsDj~)fwdE4kKlVak`zy zw_imK3F(bEX9OnUK*Vsw`8lom;FyMF%j^bF%RXp_1E-hT-YsBHuPUeNs?vR)_w}cnrmcr*tN#WT7;Y!AWo{i) zyLX6qJMv}B>ETz7m~O&}V3nbSh{We$;tz%0Jd>h;0Ly2TZfr7O zq~rlvqB71mMrwX+LidgHF=7B_dYwZC?Nnd7c0U!WGeA$FyHK0>BEoio(UYoBddcrF z-i?BT;+M-t($4pBMqB2|a0%3%uhZ&2Lfu=pc5%kSPALfpN<*|FZ`m)I5n`9rA&k9vCokT3&#Vee4`rpKT%y^fgsKxG76bc-q z1OM){{p~X6>btUT+P7!?mP0EanJzxRNed+)MD0gq*USAS-VC>MG`e7P# zFO7J)S7Z(9s2)gDc6#@ST*S!k;xp5suwUd+gA_~eW>fMP%d@zrPMC@S*e5NxNkjoE zu!?GoWC0b;uVZ=|Xc@huBIfZ@VL^H$V^rf*7K9P<_hW0Q5{PJWdnhc5LgrakStM3} zswYZr*NCd?wML5!u+5(9og(SQn-Ydgr^4-m`iI=KA|OO9r^X&Bi~uECj0p0chkl~>ks9ylA!=5gFx3c-P3 zz&~X&L@njTGHg4Z$*D%LfqO;lo%b6B_ZiV(1L{mwHYt1bz9W5i8Znxqb8^STI)6rs zA>5`_qN*!+zJ*%?iC3op5K?D?yjaFEaO06JP>Uag!pGVbQaH?wKq?mK8Ryr(P# zCjNwG$$l?yk9(|!Uy(IyWC!bZq79&Oq)7judy#A(ZI!}>S*gey!Qjmdk0pysOF4pn z&mPUBjgvLR@$^NFDqY4gOcYItWKAijk=8fPQj#>Br1%Ms_Piurz10JY^UaHlGT~o@4n`$$VG zZUx6~f>m+?PVMx`V}gNI+RG-_NDvsY;o`ldMx(u}ghyQmEu+8!}2z9#5{;^7$?I#kB=^f1m9Juhd3a@aXR8q7Y{#Ie& zHONApQBb!TfejjM*`F=pcVqdf)k@Sn#yXk_$#ZI>YTLVqtX2-%2W)1*k5DTb*LGt0 ze!%!i#Fzks3=@^%t9QuKxQJOj;nSgUsly$^Qyf|(OGONy=y9hquKQKjuz0g50aI@x z;bF6F6c*8(8uVP6R}a3u(whU)d8@=-*wF4l?1*F z`|LO>i!qpdyO9T`tNG|R{P2w0YOq%)e{#XJR3>qPe3X4b|2JUAgMwH9|AZ+bcq&83 zza||v)gB+|cdFC}Ff`yoc99?Ddsv5jQ3au`yv#35$5scSRs=<1k~A+IfpAzj^m_F7 zne15O(5vdZ8AHM=5O30*bjRMYXye$U^vn_0PFoQlz~={8{|^i-37@lD>#+cSfbf`3 zKGB|UQYP^h3DL5#jI!!iMH#XGED_TmbfFX0og>@crIZg#CoRBheK7WbeT9r6Myqex zr?6GNp<8tYcrCR9-|r_g7#i3w8NX6#>Vd_dx%%1RSx&O#s1uBB_KDJ9JYmn$c2O#V zzL0gQ8Z`X_YN6?|niK0oha3VtHIqSlC(6Y!Tu<05N$+vafnD~A&@b<7@*y_RJ=9oo zCb=8+1E%a?+Ymsv(3NFX@w#WmXrAHvf>q&+{FQCLQ3G6RnU}Yj7`Y*XN0JDHQ`7*1 zajhQ}I{mI1hV}IaVT5Rj6ba}8Bxl<%?%!7PZfJ@Ms`aaq1~o>rqFf>95kJ_bdk{jQ z%H4lh?lt6-(FkQvlxTNJMQx&X{|xvkeR~0mDD|QXX<-9QMJ17ULgg>JqHOtIxvRa1SvHlLbhx%KLZ@?x?E32yg6&a-qK^2fuPm%1D zwlr-e9dr-ngSdeg2w(qxBTGf-~S`P&Vii3<6R^IF!MqxYs*e3ys7)C>}j9y;e3Ymy%8 zZSo5DT4|U--Q6!VC~m-86HF?SVaABlW4#R|r-%cgYUh8$wPn>B81!GfbXh9*8wnH$ zC=VhPKIR`!D6jBO0ZUfaQd;;YVpCM-Lj>!!yZjKf&cMm>3$%q11?HL=g??eXLmLG1 zOVdj@-1c4M@eP@oqF~@XZs8rdv(li%r7wr_WIDONY}|OCSo{9G-68iP;4(Q|sd`E^ zsSNCj_AR!wUEMx9`2-(7Mx~Kv<0wV`7T1T3#yW4#U{!Xa0em@|1X|UA;R#hB2|LR+ zv5~Q;wMavqp!b{X^7@Y=R7L~`0R_&`rom3#2_G}3*CQA&GLR?t5-&21nkd3HUAJEg ze@Mhgt4I%@j<(vKUxNuT>K>&J3PMJ+;|arMP{g+05}7COiD3om$?Jz`9g~O_#OzNyI(7A4eH_t;ZNjV`G@<(+W+bf1np7 z^uy{TXGXTWj%yx07BIDsIR&GkOv2!Yh!4*z8woVI?4<@&lLc~Vg~B{N)Ku7*E>$Ps zWw6ND0_4I|4j#$-7fMXke?Q5jbX@#7qL|SF*E(QIVnOkg_mx))U}23w6a8t%#6)CP zc;xk&il-xVWPF078M`sxA({!LXmF0WwBp9!(A*4$X@zvx1Bo`s=oZ@U9$+!_lM-?M zLjub;l%n)KnjD67CV73}c~7PJd)CSCTjZ`S7JyZ}lqs^ZvCG#xx{oIOxBqNyB-^OZ z{(AK6t4YjQ;EOfjx7~C+PX5HuV3T7L~H06as*Dz z8!4p9Vv#zO`#2lEHdZeYT{!+#?h_^17x@3)qEt!oe^Bl$6%u@E#~KLie`)OkK*-D! z3J9nXGZFCu^FNcs{|8sP)IQvlmr%cDO-b3(qx|oGR|g;w#}XjNK}ZXNfl?sf10g92 zSaD5m5(g%7Ib7p_NiFe~*)%QBlSxWxnTuQmhC(1vnzLM4uAE!`Bz}X>+U0tg-Rna- z=s$%z%~{Zry_6?wpO8Q^JcC0j88zB=Bh%8&R;NFZHporrFlR zjYOGOLaLoq7F;E$8>x(3j}5@Zx5^j%PAlK5{_4c2e?thnKAtljN{g$lAo6kMEyAn6 zKxt^5mL*CE?;25OOc_g#gqv}h545GUwBU!Q6fhbao5-uOlGQ>rU9Ipkxz0@nxH0b{XN6{@_k~2!7h#N#EeO@@Ws-cmd3<3TZfHjAm*O~ zB6SkVgKH0$t6^(=Sqw%{Y}jW}{SE2+$TzRvau%%Ybbsv_N zzXV@F{(vF&_9FcQC9QQ7onZPASPlNB zeUQT|$BE`wvE1lSws26W;a=Y{KG#y4#AK-A#d`e9`m+!h zFl5O*q3+@j|C;uXTwpsFbZnjsGR9CZQ6_arY0=0Gcx&i)fD9$LT3$+Vme6>)ID7eb z;p&1?XK+Qpc?gkOp~}8Y&)hUZkUAO|P{H6-y4laN;&?saUj%fo9A&*bX|4=w0dm4b zVscpQp1*K|L!hoOoajZK;P-`z}ls=C8#~t;sAcuD2&+s(7f! zMEko4U=7qE6Pd6ob2T8$4NT^30d9pwD}v~Vz!d78dkN1ejV2-V5eDV0auY|)at$f` z&@qbDR%Obc=E@12k)Q>i7~69VQ)U+ahAv*A<{F{5Rip2u^>TprPQ|YRaC_wbmfI|O zJ~=joG6HWb7sFGxQJxIkskO7(xq_ZhBEe6e;I;)3j935uPZ032-q_7iK=Vig>p-15 zv@hAYDkR;(h(KqM8cIlHg?e_d-Jvgt2it_fUc~X{RfMY+mGh96hg5;{a(WV%7>S0~ z;R0!{s_u%La#+Ou+&3YN3gY--oZ73x1?qKn{6_DEH-*B8_1}FqkgXj`bmnR*8@RTz zYuG)v*DmWX{~nB?G-?hffTZJb?EN4o3Dgfn6T2-nsxleN9OFq!54^`2G?55FPaeGb zwUKK7w10)YIsBH*>&!Gtf@Dz#iYzLenZW8#xpY{y7>erTzBR;jy|X3*m5p0^U`)48 zSpCbL0Ky5wFq`Dean5#k^;H*-*1KS9Sj&V1b}ATWEp!;|U)ITSKf_!%@-_C)StB%*Szaq3O6}KWD z;EsPIZPGdKawSz^ihPy|V!8@*131O8qNu5Ls(X`-g+7v=fEJn2-`m7oZh1Wo$@4i& zVeVoTxmqEib<7F_ z9a|pDAyM*iivgnG!^Roi=|9)023EYhRLpSl4j4yXw0Q^Ixa^gBU&#H#P!Xcu!QVuI zoPFCfOFv~ffXLOBQY&nir|)PZbZ@DNKBOr)8dtK7$VR&#E66h1o#9AvJfX&Gq}Puh zOjaGy@ZP?1ypak|fIjT63m-IxwENd5o2r-CyG)FP9oBJVrt>|^N7 zluAt^!vMP(bl-ah&e;5{KsJQ&8||a|WHeZ%+oblnXY=n(bDI`naq;vk;4rSgsb9d(LXN*v5K&@1+la*7A`L&I-ybxjRW{01*cYwU&;EhES3TeaR*IG$o&qmmmOsMdfYG(>;Bg zLttMq+)XJ@GOmzlI-4PDa}cM?zB4q6*70!{P_&&I&G0GVoU9g3DM(SvX}_YPCa2^B zRr-}y<&B9;MRo;6xKe3DInCbwrMqQEL|6=*q~vlKx5?GsqHK#7r0(J7_8Xs@WQ4V_-^KIJ@HyiS;l#m)M}}f8NUs7dZp){Hi_P?%Oj`8 zhqi(Z1{ZuXfXi;S|IKcIMt{=LkE>e*;KO*HbwpGnucNMtPr%bzTJu4vT>!Q1QF{@4 za2M}wy;dP8mj~8ym_FH?Ww#g-83%oRKvHJi2i?yhqfSdsT_x*;<8z>O{&-={+Bb6W#gdi1<28l2=?hFJL z;JOwk4kV)mXX)qD2W^-{T*QyxP$$tFq+VB@ZB07XYihcceq^9^71_jb9N?lr@6vib0p<79e&{VTN=WYrG< zvzg0^tie)h%BV?dcN*H5RZKGch=_>{F!JEaU8oxTb5Rz5%|P}SV`^E(XjXNE1zd1f zf5l$x+|N^7##q^in}ObuUuq+JtDKV4%OeiDBP4_?d&)y=hIqate)_~Q!+@+AZ*JbB zx$#R}bK+jc%&oL{JY$ja7OGL%(SrsHIZWA!&O+UlmQO<0ywKY5*uLFil)H=s@XqYj zkG++TS%@)ZDV2WQ=dCKx)r+$vf(i!y8Pn;PDo9mVaesHxDq}lniFD}anDwdCp1-l> zJa2CUK{xD_d>qT9w(VT?bYK1FF;o@ivP$sEbWMHw$Y%7GeMN?K!mB-Ny`)qR)c*#Z?`!%uYdQ)5XD{#(>>;?*dn13vuO~1BJ+- zN9C#rBq{@(+PqUL?9;^nTHqNr3vlRKr!I-ym^HYY7#h$wtkx&D&x&8wHn9OX%Gbjz zTtV5Z*uqF1VEZchX-rsxT-RoTFEGxFnn_f_6)lbfU-C^_!gavg(|^6wIbmLsLSt5 z64CD?y&%XwHneuir0e67F|bk58W}b=7(~Nk*E2bYrORpzKl0mo^yUwsPaOWJON3UH zXMxb7VCUho_u-Z@t>;+bNrVv5f-r>93<3SKF{?hdaW>a-i;;;0()?!Opdb@2m1;8@ zuJdsnzZ^RrmV;OUl;AI+aec%vm`mBowaU<^HYWA9PwYtvP6UVQeMWVyu$%pz(=cFv zjR)=`lsMB5DIL(oR*~9H500>yGc$8^&)>Pbd)vFkVGDOTyvpx`5>dHE6JL>}0O`}x z-jI8hS#7x9(XM7w)^l=t>J{Id-F^K#DCQZJl6@`Z=gQ&$OYoiKtFjW~Z4C|UF!9FJ z?IlXGk5w_b=gjrwA^jt`;K@!MRY1>7`P^8lZMQR28Xk!l8UhNdm+J0w1s3XO&^vZ{ zZL-=7{p$=g`Cw&|MHE-(Dn`H4D+S$`wElo#r^zzUOw|X6Tg*wD(`Q1}s;qvw(>1Zw zA;wnowr~faw~$4#dcVB9YR)P`Ww4QP$M%-x#o$#n(*el@UkOBxE{QK_@rUkdwao~` zZN(lPyr($Auq?!)xRF6iRh@8}BMW|?npQsWa%Y~8zO~CN53r#RS$j|nB)d=eu*c4H zfMlX4BVd#Zrbl~Q%pai=BUM{r);NdYJ5p=)7pIItkU9?v1Wi>P}NzJpQmrsmJ@BFe&%2|KX$UHRy*;L(UMs> zZfye`ZS`k6cXPhwGz9g?S$s0_c+5h6D;IBztn|!ILPq0a2F^*T|B2QatUfs#i|m*bIrivqISN(Y6HGEo(|yRM#7OK%KpTWt^p7UFTy69MH_I zI9NS0BK2am&IE>l8MV#aY~Z7l)pWU>rE_1VhDRg>lJ6S#NKZqTMMo+UUY?=%K_uDHsx;l1{$fE-5F_;Ygdv?Om_JU;x!E%mK*35`&Im82TR1CJlBKdC*#^?udfmEVuEb>t#^bHW zcA?Ae8+cQO`>g&z3M0ktE--7p!tzq(;w8^wyXCiuVxcPD>CNq#S#x?Xgf#N#gfuC3A5Asmk;y(R3Ii)TIcQcq^8NJcs`+6uDf>6uOZ@bbfQ4j68WILR9JJ4>e`u%Qn2c;S{i^7Ohip1k$4&)Di(Msf0h$kRR zC*ZXczr{->l(Mm8j#!tLEfpI1l*fAu*lumD(~9&|6K%mHaRuFR1%?Tf zs9#{=oc8xyFpFNo8aaT{%79l#5ri4~Ff(n0nO6Mn{eV}?#L%DmljqGsW=Audk~o?O zBuTrjFi+7__Ey)_M8UY z*~@2;=T4j9hl$HK)aOr`XY5PXRkH|+Zvr_vIstWIP0h^(uRCfvj`w2Po<;D_i6&>Z z>e?bn%n!W(<#F2rmNUlxpzMTSsSNrM|AlYVfNGV0QFe26Nu$EJh%iE}N?uufg}V?+ zzItO>A#6-{urtkEU3uom_REUPsTE19wM}>d+kG)uaTgS&!4lbJkTUot+vm z_6Y0%#z@O~b0bfzSG+x*BZmD&4VRqVg)NF`!mnlbTVZxoHpsTR6CU2;r8fi6C;)-m zjIX&?4vpn<)0@{O6dW4Im-r=n>Ypr-I z0T3M0^|&egU>jJ9WWs^d5T27z3cyfN6xcw`ak+G4oD{~AkS-06tGiCTQC?`io>9T< z6ZHt1EHMdMzKZ8$wzxqz=nZo{xRy$P2RdW(!L45_x1{y*Pw+2Bokq)Uy$5!ay4KT3 zm$fP;#`<4gOy+&OA^STUSfdLR?R`abt=Cyxq`wRJB0Je?a7LLWw>r5FQ;bbCAX@} zcZIj2?bTJmoa_%KSDtUy(%@Z1vjTGq8mN&6P_;!<_txEoro^^0vH3lvnvrq~Wah&~ zPw~U{C3@httak8IN7;nN{A^dL#&+3Jur8+He6>?*>cWfkVd*(1CV+w8NvrTo4GT(h zA=WI_QFOkvmC^yG7c5tt&cn2)(aq7xYFE%dy`0W9`af8(jMM}lzlB;-9ZnC}Se}$c zd|<10UzMyYnStPr%FWib;xmfWdPZi&Tx2gI6n$D?A5-8SeMHysn<)1vIZxj*60{O| zsg3NY-_)Vi{ZXY5@c^T5SW&w)9DvXV_t-IYS_2g&1^gZGA_q*f@<~L^=zZ+!>5HW_ z(n9P+g_v@!f!sF(I+;HV3Xid4%+)qmG_?$EU_?(S<%ckrs-^L>sJ4RrMZZ74^%GjF z!p$=NJaq^r6x=Z=%!LfA{qJQ|EwU-`0taeI%K;t)|F@?Z;R*Qh|Js``%YZNcm8sIe zpo0I-pD}}3!Tu}Jl0eZR{*^bv4Qp!Ea(rAmt%J~!N-JAc7b5q)kK9I*`Ld);b+%f!ag^@ z3U~R{kJAm8&tJ840}~3WkexP$6K-2|@AC1P38Hc*PK;7{Z_r1ncj=f-EscCnSKT@3 zRvc^Lu5~g0M3qWwrQYtY;^TyjT-)TwVu;z+nqSl8qTw?z|5R-=k+P&Ws=NrmkE{^v zz8$Vuc4tQ4ZuJ|}J%fKUWcY>3Xz`8k%3vA{dRNh$3>SH1A~d*y7-rZ|$OQQZ%yN;kX?yj-}w7$Y5Zw)hyu58GX2Q%y`DCj#3mh(R4 zEb1~o7_=A>dRkqMEs0K;AgdOjg)(9sKJ+Oai4|g6y{xas<}PWaV?nTrp)84xF+m^N zHq)=qj$S*&Yr?dl!<<9PJ|-dh6@vY*MO{pa@J zN)t^bPHnIv2+Jj4tXwF6oEdg|aTEFGIvguwvcNV&MOjl&R-s>;9&ap7*YC)sB)Zzf z%H$Y*-sE^qx|zx+A6J@kxM&$Wvu)Hyp8J=hh*!U~(fIPw2B(D(OS|)5zF8c2(SFXQ zJ&_!6PMQ$347baZe0ydsZ} zFl4o#n5!6QcHb1F#sMji;Xr@9y$*Y~vs3R5a;UDTC@Aj{0k0ARttH~j(kX~>$|GsW zLNjCAqB?!SWvNo8`#UyS(}E>A7p{et27G}D6BDvH&!Co&mB~Dyof!BbS{5yH0uzGT zs<+|Exjd1UK#!mrsTMP{GE9W0Pd=1*?l&1Y@#>{IS|x?ctSITtZUmmVp})I~e3^F2 zU&gbYQf-WcWF2`IZo^JvOdVExsAJJGtz%u_To@77h#9yn6}pfp1?|h(L=-Q5*5$Jb zYaiBAXBKTbT!nBzNICUHZeqY8hSOY=b2j-~;b{9sLv`N5h<@fK38xSCye;e?Dj#GJ zwZ#&C*|xA&mNStLh_aLJ`6C6px?>jsJoF4drRko-9L=Kz*_Lz*;}rFTfkPaNC~o(Z zMCRFFT1?Ral2yJ~{ILFyLI&cgrS93IQ6)U2{EDi!)8Fra0Q5uIQJkn>1>Nq%l*bcU z8g?;8&#LRB&|OqgZV(RHu6aoC+Qkd0`du>J>Pq@f+Nel}i*yTW&}X*qlnGkuaAqGW z<9;2dxalu0@_wqB-a4apJ1BZqrMmV}gJ@bfQFfTbU515r3vOABiQSR-&>3%cyzdcJ zJ2Vo1k_VUpPvnVy>0n72FY~-}Q(yXWroW||;4w8!_y$j(p|F-^leU0fGmJum*HTg)`50G}B}0`3Q#^ZHNGcOY}T)!z|*aBeg|d-L#2ZfWpY$i z1Va0Y^F1sQJ1aAQ91|;ONWXA&+K4@KxSJ$P_?;jMfP{=GEh0aG+5V(BDwTbQ;)(7L z!_g{;pHSYT0JrlvVldM`VtTAnKROB16C#8aD^Sa%_qTY%@5Preh=X&4GAKPc^_Ux{ zOD}=52$$^%G3-_LS;ad)gCwsbO#5qXnv-x*fObYg*2Ui=v9eJlavvCe_o(G?rB)X{ zNeL$g*e}kG&#@0hiQSQU&L+z6Semsk!vFJN6NJ@6i}*y})pt0CEQ&!?$tpsm7XCw) z_?8>AnHe^C+^j-lQ)!&ZC)Lljl+iPu(Zdiu!8Z|iU}JEKM@kP*bnV~GnOBY>)ChNP zK(95T36y7``F*q&A2@_9&H}Rb3Mq;qbwGRsz%{G*;48GvmNhJl)}WW|fHPS z>QA!wSD!JgQ{9on`HvOsAe)HaB23by%(#vVU_2&$9T7n5a*GnA&&iF$jXI3jsFAc0 zNKoVsvCs)WvJvL2+{=*H@>5m1xS z+UAGV22R~XI#5G|j{2i+0l5hhwYRSVjq9M*&@(|=#FQm{lj1MGI%Fk}@G7VavjPWf zoL0`Lv#JE-N4Fmtd`V(Kp6y)Z)gRm@whPW!Nso-@GVaqvfGOL zih3+Y#XPyKlb$lQC#4aP24Ao}E^Yi-K53x)Nc(nm-91}D0o zyIbv`6$3LcuViS1%OhR@STI#rKe!fkVxu%K)33>r-R#m8!$q#7OUVrQYAAo90+ZH9 z%~wfoBJg}IK12~3CsBeuJ}UVoMt!+-|Dq-sS`Jo-Q4TQVETwK4{wDM4c*XjZfg0Tw zXhkQ_nBGZIm4D~@rsaTnhysV3?FS6B`vtYWp~~-yM}kUB(HU;E`HEOc5CM5hyv|$-P-Xh2%;= zw-=0%m$X!(LmQG5eT*qOtyEkm_vpV|OtHvL!kh^0|jDnq;;ML7Ue+*96JO)qWpvPP@ zovi(d=HfdD7}`D+VjIVZEtbwG>3P_=2cbECWzJ9L*>kU>irb6$alY?x461xr4@{f` zO|0Dg?Y|%jh=P#r!@nK83{+igx$q|3&~W5Y?ZgK{k){Bo z=?9X8`%vG$TwGtm!7@)Ro$LWUlxGde=^_1>;Fg7Jk@Zy=e-_wS0d7%VT9za5t`b~F z2P)eZ)_IX4El#!l7|cJ3HF5i3Ub|6b6*ojYKG)e6uqc{Wi>Vpf&b`9g&I;Xlh~Svy z&#td2+)K%!tW=tQ0U^FSFKR{7--}-IR=SvZ77Hptj-MaxWGRBFxgxO!UH(AyZWu|l z=dN6*q`X3!mwg>pgqloy>Cu=rua_Mz`j9nZiQFV1d&O+ikfrgGA$HWAgWl9R zZL)mBXelhDMd1{Y@-xq@&DIgGWOnXQJ)5fojK9)&%o-g|ZiV*Y#m)5JxgL-W1S}Iv z0<{gDhnWZq(PNvRpMfxHo8ZqEgoN#&S&Chb{^ek=V!3|M+djQu&CRCR8U$-ItL8@` zHfyR#1l|jkC5x^SR`$D|k>#v+rOjb>+->vADmO#p9WZCVjyy~?j>;_^7TaqJi%^;Y z6y8zvakk-sdOG4r9V{+Y504;B*e$CogqDgn?53Lwdf>@W^9d)eJSv>|W&~`Mw`Moz znqDRjtrL2~lf8Kba_GC`rlr-=3@1*g%bGANeNhNwF{N<0fV0~r+oGxWNULNIqIjhoE4;dGdU^HG*Xu4MX5!v7I zUVBpgp0 zgWh(SWgQqHxDi+eI5hRkP1(1=XKy5jR481r;nFetA}2GF_wD|$2vk>A#4B|g;>sR7&S$QB(N$hlIM zzjl3`1MT{fqsn>4M?vyfv{E_Gmht2xoUBQNz`1CYw8%K!QFcw3R)Hr2j8-qmW?%C@ z_{@LHu$lylKx!7Sn%@g1=#7=mYPug_y3cT0mPVF`(&lSf@LZKhohjVU9W?sq^soM1 z6qr%+(=C)WsnvWPK~Zmne{L{(2IJ)a6;Eqh%;^NnDzSF!@}9Gbz}3xvTA<%yVmgUQ z1aCmR&>boux2EO={!9}D$X+p9Z4n^k5Qt?~O{?JP>R-Gpl)<$Nz@2Ecv-^=656cmV zPBZa}>*f)V!_CXx1D4fCCL3M5o_=)Im46or@KrH#(L#Q7jGnTotnauAt#pxUDjZZO zxG6}s^$Is~mGSTBZF~W_6EaxPnHB4)bZE~W|EzIrQJRd!PBT6Q!0-0y=+h_Utu(Xm z{&{(tFT=EIvwjh4LEW)?dj6GMNv!rYgm6$r4b0e8mKuRI&XWT6^@YIGIlLftgS{47 zRF@313SM5-8IY+DF)$Z!$SF{m9Ju8ZRf1P~)OuD*Nhx1LHo(bKu>dm;mkghi=P`b_ zzEfKNRhP@+M5-+hC=HFBt1~KkBKpLjJF=RCJjzwzgxg8&L+&T$W1h~lJ#44iEmmm% zv)z_lQ&2l^dR}aGiWtQ$vCf98x&l>S*i+Uyk8%f9SD@@Iq^G% zv%~e9lEp6GGQ~kz;fvRw5P*=D<2Y~Ts>9#Dg2`Q80jRT!B*%OMWEa*-yv_C<9LqB? zMo8?ozIbWmNo$jUO6l<5cA$cN5Nn!}EzPl{ebKP>xwZtk%+aojcpgdGbGxf1SIh@PM%r=Vc2RS)l?AXGh*zY*&n zsV2wpaG7grqM4!hsYXgM$W1`D2`$=H?VWv`3401?c=~g&=rC0mEhSN9N2x(QTFs8p z=9PHm#nvkE_<%ySvVmSZXF#bY_8f!2?R~Yx=|G!zk@ca%<-{iwRU&zt2A%+^4ODdU zSDp6(WUAGlGDwNg9B_n{IHFR^1dXo_j=H{PB>Pu08A~InN6OVt-nNZMoBtnI{}`-E zxGW9BGt0JZ+qP}nwsp_4ZQHi3S+;H4e&?LMBfjrBe>fkqRIAigjIq|CF0DmAIsA8C-;RAA86u+yT;tXDy2_3n5XyigVNX`EW zO{MmEP^0G@k0->Jz_tX@lvhfg=KIZN%vz~5L=xG9f{nM{9GZSNFm~c0yPzXU+&OpJ4m1l$3G2V zhVgg3GL1$F^9bU0dypmGPeP9?!ykfL`BDYWV3ZPYWX^$c11|Xc);&p{qi>F^rd~?3 z=JWus+zSTCyBclR{5NNVBY)ow|cjD`LHdol<_Wr{c#?NrnSQsvZp-t z7+bheg?D&29t+$|emYGzD!@wAHfase0n?64VZ?UCPK$#kBJcNrr6kprQZw3!?@F3< zf697z2@bQQSfg(Ce0IPl1I38p^;L76&|kv^und;_MRM|AYp^i zt?Fm2VmpId5giA(-jy#W6BvnjMMG8zXIzr{F1ly(ba&3FgN#@9dtvguM7$(i#GZ(q zFLN@9L4AFnbW3VO#VA*UM4&9FC+g4Tj^L2I#fALdhofS-zSi;$qt|f`bFV5-n;mt3 zxl?~3x_vIuK}at=<&Xs?;Sd85Gpjt{Xn5hBzmY1O)$Zof|M^0kWlliZQd%Xle~zN2W!io2(-`TDJ!CbKKH?S-t<4XXJK1Nsw8SI}E9{h7@+w;?s+ zUF=`0(~G`M)U4A>%te#=C!kwr+urqzvPtod|I=NjFjG}t zk6WBswWJ1b^Rx-K4kJoHjI$=V*F@w;Nvl;t)D?o)6@nGKedhR+EZOTnX}$ z75?JnJEjxc4hGYpvLQVy%Kx`l7AO;n9>q(z8P~XX=IX+%=)D7w%?elL%4xHiHQ6L)Gh1|Lc7Q&ld`^>Us4%?=*Rj?)j*9Q@u%3L!X@79uPZv`P6j?rOW z(?ghv#o>kPn#C3H0+ux{1lkqy1EJnDPtha5nF0IjR`MHp-|Yui0BK zE{Ly9T*9`+v#qS2AY*8y?RW@ji>}H=2%6c-DXNm&ch+vNZK>thhkIQ2wxIgRjmt0K z%=M&WpBpsph#%t6*DY4x&st(iR5_xeH%BG<+1aK_vJ)`CC%NoQfJtceE={b+IgaRH z8^oWcG_wZ_t-quY1#^pkEOd~2&57|vcC=%`Uhus0)M$J9jZ3bVNn{pJg|uYsg^o6HR-DYev0A7! zb)>1>nGM1X4|#QYro-SnrTz&AfllpVLqcm!tOOqXk7O_Q1r*%`^w%$I=v2df=>N8bW@>?< zQm1*55CPe08}=xw7(dzb*G&^m!DwaB5vq!<>@5@!Eq~^Lv~-}FlA%ydxp+IXnYbIL zC()pwwr{?FzuUa)AnR6;@EPrQzV99hB1ey;VMM16mGv@rUOt1VHLRuoGdF$sopk2D zz23m{fz+YeL%3wx5m|eL*h+JsXe7=*m=c*r-vhGiaQJVXFwa23fk%q!$z`n;;>wvs8r;i$_+j;&Ha}Kh1^>B7<0mWyh7=w0#x((OjmmHP#MR-go;x?DX79dr+ zQ~`qY0-;zSRKN~WW`I+|drgEaEM*-`DIJ({Xi5pAeCJt5DA+=IF+5Tduk|Q2OkU?F zJ=GYJIWF6OJ6pF;>#-Ya$h}C&Pe{c6@%$3MwAt$3ROU*pD&Y&Euw4xRv0yNRM~ zl+qQJ$*ZqoWw~xIjaJmQT4ks;X(x`A<%+&CM0K99W>PI(D;` z(fTlj=I{25k6t0Hg0?C$`XJwz$;yC?fJkpy`*IbwjE}^iRt*%=h0$I_pb>Su#(NZTEKOy9#te!5|>grNXqn5yOR%u!9pnKD1gvIcArff3N4c@}r7 zQSle8KB8|@3Cc2f>b~lfXb}W?;+p&KB$j2n0|d&Q%j#bdM`9fR^FCS!R})lc6nO|0 z$r)vLxr=7#N|xjSlMe`$DCe$Gsc%UylGR-6)Ww1gH^*ZZUDCoX1exYbcBaP?!+Opr zONv?IcC0vKCR5JyMONu(Yc#-nL65?p%UehuxTx~81Y47 zQyzb+AwE1Md-7V1<*GRBjM4>BqZfGYCgFD_!Pk2YmbBu;*l@%Jdd6I%RUMI8=ZbZ8~ z8ob_szp*X4goa7F9<^vP?6_n0z zn8fd3t@i-uyKm!_IqLq`>VODWG~)3!&brVC5mia?U-<(XcM%ER@X~=UnFF32xJI^U z^IIBQrA23v@_~t>r-DyUoxDZrB30L_(MrQ<``w>rhSDseNspx7l|kGq{r^`4z?d5Y z{%h!;Bqs>s|L<&$s|NlzfQT6hymiP2cpCUWXFfqQaPEJ_0oCUq`9S}<%GLmU{2!lc z(kv><%fBaGL8ac=f(W(}*aM@1{BMrfNFeC#_~*Tj1^fTW0$Rs6fp`CNBRS2UsNKI5 zk_MD;{@>*AKQGX_`5QR$H^l!;Si3BOs7TGO1(pZsP^xQHf-0gc5*0V&fcg%kVH8Np z_Jc#fXQbKGQq$nMdwAPEmfk}9JPS(tFx)NSn`UhhDGDlg9lDvjAAcNYreC(+7>!1n|vLK7qi)GK>uTAueV=IZjMT9n+}`1d7vQGM?$LaWD}iqHN)IGg_t-hNp& zZNLV&wkE4dqHs$fkI2tPdfOmooXDLU1TieL7XkYP)U>RJOhb#b9oUl9L%FMkwtG6h zTjleruvXGFV300Jbc)a#vp@nKgrx}D+!n8DcA#7(DAWPzYmL)6kI2iC z6yKbZ;0slpP`>HI(L0~ONr9Tzy*>-b{@>77&giWU+@YCe2xPFT#g$E`APpO*UDh~{ zK(zZb*KKVP)+N%aLhCD#`7RmJ!&RcXk`zs<6zpCfn{~8Xfcyup zP`#rIbpLZ0s9~itnt}c|A_-Rh{StITz+kDp5=dZxZVgWl6m!fUS)??R210}wCo~YP z6p}iSS*M~^5(Et_AXy~`WNGd-TsG*;H4biUIO_}l7tLWUtL|piN;VxxB<=Z%=GOIP zt;&|Cf@ZColRJF;rx#ylZxi?&lmZ_32j7pWkM5ff@0&5=A3kUNKTEHn0hIxa`nh&z zb7hDC*|d1S!3gXg-gvv3o-oYTYLQyQ^&V>^FP+vJTWiaR^E(e|8F}YTr96ROHdX87kD`gMRa3C|jP_#qwJU z3MetuNvrr}R@e>p43};o`3&tzrMjA-pOM%lEYz_VbbwZevS7XYnH)S_KMgEt z+EqV^N=iP2Gpb6)h~E#z0+j;S7$M0vHHEQ2 z2#E=3swcBi{Z#=W=IBAol=9S zd|!;gQ1mje2Uo3#ABlB9tc;Mtf+-9e^%4s__IOVJ-J>wX$;OC}(HvFh+EuZb#To)E z+M|ooY+a3p3hm&J?2pX|T0F8JXlhO)q^>`NH0#Fn~Q~IPfM#z-kijAqcraX1B$d{ zdxBcEy+xgY`KpDD7*AS5Fz^PZTfIMor~f!h#E7qVEwv7JoaDAj8B|$$bb}e_jEn64 zKCe!IvIc)C4i}SZ)lM3g8D$56TJMb_GlTw+jR%4_KMM<_jS%(pK2Wd0VfET}6`d|* zaUi%6ETqi5Gk%rEVO2--Meq^A&Yx5x22ZqFnGgO^X0gaHCbgWKNPTgMqv9yiMftSi%+oV&(J8Q2 zHN-03!fF9JC^^M!f|E!;ZUnicmK_QHJ%MlvVTxC|&YdSJVWLeT`Xdhv72oLg@2Ptn zn)ZN>@&oEjm|yg{U5g&*CpoT)9<=@fg9wmGxgh;`j} zq9F`XDJdV;!`)Hi_NY<+<>kIe@P>XX3M+m~8WU(4&=O=R?iCGyLDA2@-tMBoS=lb$ zlk-V-Hj5Ef@!{2k6i0VO-N7)83-Q>jGdXLaVN~{|u6!gFl=cM1y6$HLow%Q@1J&*1 zOIFsr3O4*3u7xuEugop>0k?IXpI^bC%HbF`Lc`!#`O1c=n0LBk{J6}}Q@96nU2NtF zTFG(I&m#W#HP!~;vBy@s(@lZU?a}Nq0wr&)xx)~FUOz$s8wR-tMym#<4I>i1V z2VTIK$OOL`1rM@{g>h)e))-8~TMBwygFM&i(iqJ89e@jL6_RmTGDNR%u(@lO&JK@qs(P)=*R(F%F zNzT(}Oq}7rSI53V57%B2PE*!(hI*)I_uitU;Tl;W>Z0O=f^_w%Ih1LM*i=7|=Skn& z95T`Y`@2Ol)-CaA*nhOsuN`1@%#Z>S5vs7I#2f{P_+?6iEwWXU;h&TwNpbuDC8Z%e zjm=Cl=|Y+m{~;xHvO79+s#+7ITZG77s**cYz!~TfsBr>kbX8BYtgQgWAKj?hptq|d zMad6(QI|tAT$9sDbX_|HrUXgIjvSJg%N}(v;5vnn5pHl{d5Iyo(buvrc!?u8sOjlT zb+rW$+2?oWN0bU3*RPeLL4MtYgIy^MxM0~7h&`n=Em7{)^+*)qp)X$M0hyKT0qpez zSp$Dl8Ft5i&~>df_rp_4I@s6vBFM6;vcnk7C{|VSMq5Qr2LK9t}p0Q~ItqTNKyxQKubtTgHfPB$iOGy;&F>KS?u4MtOMZHMG8NKLr$%l|= zM?+7`TLkp@U|8jKq>TJHa9NR!TZ1#{Xyq7b$HcbTOx!8@$TXP7?kd;$2D^*uu;T(o zuLcB=uSF&E+Ddp8ls5RnW}rt~=B4y|@H=A*bS}l}J}w!qVZy!;dfZajI>`kRarvo1 zXN)3n<1e%}&ud!S!LnlaXP#sk4(32SyI%syh|) z$0dAsLnr#hTu3Lq-oF*e(+$7e#-=u2ej$&_(*|EeMea?Y2OK@e6<(YjN`sb&Ofj?l3 z>0a&&7f_jOkoa-dbd3eHo7J?>cim;;!}9oBh2Y)96B2RX>6^^DkB zKqD$U(qmNJTjBRmB^^g0H~sX`$@FO(>bwvD2$~);P^;tp1e&g#>p#dF8t%N9mQM)K z*Mqn7k=l10;rOD|c!f_n5`-2<4-C+C0vwla-o4)cmld^^ zS%Jv?m+M3qfYby3FO2~oBWTS0H=NZMF_q97{=c};4*&w*8hZs&3Gp93q*V>f{a+b7`5%A^4Kcv2EVQ zlN|4x^fAAWw=?QLo;+q6)yq@F*G{8|as*e#dtL*ZjnpWz+Fm`_i$RZ5CjTTp?Rejt@PO0iR^-tFl76vf%k)fYynQ*BeW`bbN!$#yq57M3?Ic}z~j3NHdU6`VV zX+OCcSS%rcZGNZ)Da*pg<@FXe=;Ebh@?oGsEpsuWQ~UU0q|9J+7+u@qrto_y$4&!A z9`CL4ig4p=kO5-N|Ctb*$ISNE?3EAShEJ&Qo}+giV8l*S_6quf|_{` zLuzbqm|7-UnF)8>_S_jatNmcLyHdE9u20jY^;mQOD_7J7*FJ`6n515z|2&{HJ~#58 z_mwmxU)oG#CIzz~-Da)Nc2m*npy(KX^$_4xe;!mWdCHZ}ZYLm~+Q66@WY}jB@FD4f zeQF1k*pR#IzSK^$u9Osj)m~q0rn4~0%vYdS3={h@5JwLwxyA()RzC->c_Pb zuT2$DZ^mup8Pjy|$R0)uUrF{WVnp;(t=?Ybs@{konC~&rFlH7*)b$J&#;32@8K#GP z==M(zl-2@Gq7*(f?DnMHu>5jb*R_K}@aV`VYNvQBlYI z*|K5A)FUYR14ayrMdJCJEZ>l_e}F}rn*q~`EC^2HU}BP(C(RT2xLfjfvumyEV0VdH zwRX9*tst2sc4xbSG`nrJZL`(Z^jzV0;mITX#ocS?jVu!(2#y%bNmu*MR?p1~?Mvs* z)EQvwhv1Ler=Ec@%@O`QU|x$TpPZ?Lv8t4It9J#%;DTLdLe4y)!}23g*~D9@vFaTS zKBzBZjm=tZey4ilL+Orhkm*uoJy6LXl$oPytrpfA=pq8?MliT9%W!NvnZ$q^X~f5rayimJL)-1xiP;2(lRQ1i}TSWear+kiWM6$L#DWK8m#{ z)6te7{{;_kp0^Cif;J=(`~l>6%7DDCm_jASEW0F|?@}=O5B&~0MI(i9p0(P6URjSV z(6$CuI*U#AGwP7KZj42Jnw_Q&(jgqDftBR+Qr6YerjJgmQI>n#nVSjM zAQWdB3+Oa1ft%I}zBGl+T&bCN~sM90`qKL*j*Kmpb z;SZWcE#}_8@+F(hV2=_+b-n~%KM?%Du%o3R_-jEJ)hZzPD3TA9`Zh!=DNj+57EQ83 zvfrpnPN@=gaqlVNo?2sHi57~^bw#(vfHFnWDm&ol`VtHjkeaWjqz=KtigFGXZcMuF zLosLu3a$ICPUQisM_pdc6UbDNRv!@=`i>^1I+kztwm?+ZAfRz?BgJHpL8+anNMF~y zXTyBW_fkJ@KgF!8n+6Bxl&ubiEZ}Nlez-pEAii6nU=9#BFVgLIv%Ir~aC9OgfFIxy zdGC)($(uq0$cc9u8}#BeZeY1nx1rhJ8-jaz5Xv6Z=-lqlFIEw+ui!(7yLq`g zxg|Ri+}jgs3q1$82foa0wy*5Ai51vsi=Rq}(g}HSH?cADytF3KIJ{zZ+jfx|JFn=| zq604O(C~Rq)g4%C;`9G%_vkn`R)~-ICrVLghgqBe5RoZtEiMc5xBi)a5WIb(mVKdI zB$Ew$CnudeOUHD2Ic|b;qAal`yPcWRz2Z!k-I@J-otY0@%Cc<8_5X(qUY$6VB=o)QokDJo0t*BHav6 z0STgC&7&@xQdgla&s&l^AcLo}ku*R4=EX~A^f7BD2*v7dJBsSsFM);UOKSbmvi!M1 zCzf&6ROuJLRkYkFY_cE{?bK7Jc%ih;`dQ8d$oO^H^2NwYnENu-&KxP_694RYWT?3* zeaRm%D}8B~^XU@BWgS(#=`W$qlM%N~g-YMHcgboM%Z_6OFm>_A(o4ALPtMd>+o|1> zy1!0z!N5?*NfgiBE9&h^x&$8*nQieclO`5QBNVk2i2)xTf7IZ5jB_&f-re_mE#`U&_^GRHR+?# zzv2ZtWd@%2 zJ1^(1mgxq!*$bJ{nttnk5#S{x_YZmkblD`-0W+vyU!S*P<@>avI~c*s4Z-}@fG}mG z{X!txlXW6{Yd-zY4I*AaX2UcuE`*g3o9x=JQIN~EWKQj6QJ*Gm#g{ht(mN5#@!$R) z@noji-iD+v3w#u>jMV!;x_kZ1G#*7Sp=o)GLeZqbUnnzgw={kuSUj9nyPz(BvH5S& z5Nm)_TC-VRcQ^k$_9<*bc!+oH8OiYBftuEXkc$dOV$pZ^bz%!yQiNqVAFVA1{}Y8Auq_J;RHEEInvv& z-xZZd3x6%&7@V~DBD35iYr0tg>@(o;3v@Hl4~&ORJ|^+_zS5cOv;Dm!tN}_`_z)Gc znq^kDnrM}a!AF`^iq_N(7HZbKnpLXS-I`4m&B>Zo>ejxRHY(O9CiRll+Pop`73Y#w z<3^EM8KS#wz;EiG+0x5>TNcLWR-?SHtlkxcItc2HAaJCDkfG;!TBLA*rEt1y)CQ30 z{;p)-vFhqGkPdM~Ou=L?AYT+O7-$YO*AX8a??wd_@|Y1vL;RM}|Tvtx~4#6_-w3a{i>j$0m$Q$ZC{ zsN>M@W*YDc5k7Jq1=6DgcE=G4UIrpWMl~S10c`QRYebhrF7jJ|e+ImoI?o??ap|r6 zSkVI0q733WCpK&1IjFO2iNympJ+|t2MW1oqFU@w9n#9J!HeEKC=WGt5FRmDFF@{4W zeN4d{lmKpf;VqEsh-DvQa+icDjB7s&sP%4E^65|;s3D5Xr^2*VSD58bs~a=QxU1;T zYX_VKorgXOJj{N8koy<>{l&&n*k#Ot_!cq;u5NPVsAMtQ{RT@2dFN39B!y!0ak;AT z>lj6jx@Vl9pAVIa_3m9yM2d6nX-yOV-BhJR418B}3?E}r$Jx;|fPv9HqWq%|` zn0_4&08;#{Wuu)-T8{pp^%eX3kNT~+Vl;pF(QBhdU~icA)oW!N9snIUYYdcURC_Ex zavBiK_=Y539A`HZEJEpb0HT(TobRx|`jt6BW&ORZ1*X9|x1?0SV}%E{bCab&^~3)E zBKptM#4XT&J6h(T5(UNo3*CM1fw5bc-ht8o1JY$mpG)`QfBh07`Cmws!i|KQimeX} z3()d3Kw0tq-p0$=TATA93Q+qK7|k#*CGn^JkEnrw&VbIH5rjdfo}0MEt(E#VFo@;+ z5MgxSLWkr6@MZ~}tJM6TKuKy52S_(-3mx)mZF0-=V=LTN*^H*>%tg2Gv6t;AHR>xV zgHLf==8xkk@0}Zuotw1&nck;i=wE0NumGUQ{j=w94YCxOj4_|_b$hc0Gw;~u(B3OG zs?3SQk4cpp-i(>&%yJPM`FS0CvX6hOAqW*3>09`hbE1l$TkO{wXQED6+CWQ^XzUU% zKw38Qsz#Q~crYi@w#sW<^!2%#nNZmq+gV)85Yo7&h5eV2%R8Jz>L9k3f+pHB)N?VSBlK7D!bcr%-Przl&I<%8<8y|RWc_d zh@)c8nAFYgmS>Yr-_#6a<8~O!W~Myi0>9mrALxL=sJ!Z0aPk6h}yBM&2@;$tNt@?3nyh@j+=h z4Ypy~0!cTL<&KyvjhG9L?+B32sxhs5lUZJ* z8faYpDy_2IDvL#I1d*`T|NRcCQ# zoZ{PH?JR9=b?*4jULRjXm;k6=iht~JBO1plT(~Spl#aG^$re>((m6xcGZ)&M&wQxr z%?RwCrNJyT$YwowJ1p7)5mgFeLy~0*Ka9t^+KWR^)|x8YqvpXn>a<3wstjwas7Q6s zqGZ2axzr4JrGt~B@nnv?rm|4=)1_vkqU(EjA=OGSk+b3_i?JO_rvW-0@%{Z0E!hYc z%%)_vbW@XHY{ro=`_+$IATfH@YBPeR_iqkALta$JmaBt2%eihyP4JM~0}?&x zt33YL($sQuGF89k`2Ydp{LcopkKXi&5^hB@@S`0p(w+sL?(M>>q2NGe;tAmV&u0}~ z$ubfh;PFNetMu`32<2m;fj~Vv>w3n! z-S-glucrRf?ntj#+cO!;BNnjud0iY2o>90leNBqNJ?tsr2>?DaZW7Vt&|5;ufco-me>?=lDP|G_rqdB?;x5>s@HJUYTRL( zLwV(V0kFMvPrga@goSYtW@7=g;7VkoJ6^h@BJud6lOoG&tB;#{A3r#tpahPnb_WE7On<6j!Rj*UOZgoT~JR zn#-|t%;Zu~o+3c2|L9$4!qq6KPG=`8PyeP0Twfpl2DH3H0S6sQ87-@bzzwwte2bms z#!TYq`T}MOYXKAAI|o^DTco5~198U9Sv^=9Z^+8uTR?(wVjC=Svy~D4kIMuZ;WY06 z5{3T#xGG)EO(E=>3m-B&yB6gd#!Wc#K%y#^N<;RTgJqJeu#pD$LX?!djBM14l4GFw z^tIQT>t#|84LkYEN?q%;S#>w1XO1=V86WV>YV=d1N*jAhGjbzL{(c+{I=bm2^KGh! z+{iE~H=v@>vj=kcNEjGE`mqY;v(^XdBU$ri?Jdjxm33yQH(nvs)+y8H1FD~_{Uc`J zi1}~tP14dOkY19~Pt0P>Olm6VVZW=FvR6&ZY<(u=hNPzEy*MS05bs-71_1bnfc1wg z@f75OrHe-{(7Kn$PkOY~)UKMARdvu^a&FNB5s_N026QE33sZ z*1GvOrtvS8daxD0juFzra9J%uMj0_BB)~F^zzuZBgi-W(C%$Ff+;pd;Nq5>>G&9TA z`STcg!J}%;$I8SgMCS9XhmCVBR}voQG2Goz_Mjq=(^-`-o@4bCnZ43p+}5~K20NBu zgsiywG&24&s3Ce|L5+y3V|83VH%*k8`7KF)63rq5f(dbA*el@-*mlDhGdGQx8jvGt zn`xR@g0omKNZhF`U$e3=NSj^;BlqYio${3Cbzz^y9!vjG^V>C@pNV%T$1LW_btG|` zJS^qD+i<)*|9Q+V;;C|4;kMb&X`E+P)uV?mcoNy#ll3yY4f1Bc z7N6O%F>oEa)oU38ZzyZxHevy%Nhd)5JBttA*Zh@cJ=Ebtl!i+e;lO_aUqPuvfut^u z_vUIySL6dt`9vaqQEZw<5uik@FshfviSDKhjGLcOOX4Ij&oa5h-*lsrW$AuR}Q0!)Nui+i1Dnpimp zVkq2xR`U)`5NxC0ae5Pd$NdO}USC6i01JL`WC(p57zhqQU=r>S@cAsJoKXJd#z7#j z`3Dy9@Pliz2Wz3q<`DSQ#6(2k&(OJmuHlF+K!8UPq0a=31&f$eD2G2s6m1aVD-@0p z_&9@hqE)j0uq)wK06Ky;NFEi9RB}UGm=B>HpK1K$abGJ0KLyMEz@9!9A4g|Ni+>Q^ z=P^}=-SY=;u4e0wQ6Y<00P6Q%qt~{TM{Ec@l#<)C z>imLwM?g^bxb=~TclMP!Blr?>jp88Kt$Vu%Fmy48?Iakaa;yO4Wa(Qjij(u3Yz_8$ zG0~gmgHy)h==XngFevqQpLZN|L%fn-btplqgdsYA-j10q-bVtG$6YXm&4 zHsrLv)Bz03H7$yMR~|7>4EoO80|?8M+j(&)y%0>ESz?Cig{gIp+T6tI

KGlJ%WP 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%0A$TZYC_J?jjnH?(nQH~2ufK@ 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

CTN- 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

+ * When the connection between the client and the object is established, {@link NioSslClient} provides + * a public write and read method, in order to communicate with its peer. + * + * @author Alex Karnezis + */ +public class NioSslClient extends NioSslPeer { + + /** + * The remote address of the server this client is configured to connect to. + */ + private final String remoteAddress; + + /** + * The port of the server this client is configured to connect to. + */ + private final int port; + + /** + * The engine that will be used to encrypt/decrypt data between this client and the server. + */ + private final SSLEngine engine; + + /** + * The socket channel that will be used as the transport link between this client and the server. + */ + private SocketChannel socketChannel; + + + /** + * Initiates the engine to run as a client using peer information, and allocates space for the + * buffers that will be used by the engine. + * + * @param remoteAddress The IP address of the peer. + * @param port The peer's port that will be used. + */ + public NioSslClient(KeyManagerFactory keyManagerFactory, TrustManagerFactory trustManagerFactory, String remoteAddress, int port) { + this.remoteAddress = remoteAddress; + this.port = port; + + SSLContext context = SSLHelperKt.createAndInitSslContext(keyManagerFactory, trustManagerFactory); + engine = context.createSSLEngine(remoteAddress, port); + engine.setUseClientMode(true); + + SSLSession session = engine.getSession(); + myAppData = ByteBuffer.allocate(1024); + myNetData = ByteBuffer.allocate(session.getPacketBufferSize()); + peerAppData = ByteBuffer.allocate(1024); + peerNetData = ByteBuffer.allocate(session.getPacketBufferSize()); + } + + /** + * Opens a socket channel to communicate with the configured server and tries to complete the handshake protocol. + * + * @return True if client established a connection with the server, false otherwise. + */ + public boolean connect() throws Exception { + socketChannel = SocketChannel.open(); + socketChannel.configureBlocking(false); + socketChannel.connect(new InetSocketAddress(remoteAddress, port)); + while (!socketChannel.finishConnect()) { + // can do something here... + } + + engine.beginHandshake(); + return doHandshake(socketChannel, engine); + } + + /** + * Public method to send a message to the server. + * + * @param message - message to be sent to the server. + * @throws IOException if an I/O error occurs to the socket channel. + */ + public void write(String message) throws IOException { + write(socketChannel, engine, message); + } + + /** + * Implements the write method that sends a message to the server the client is connected to, + * but should not be called by the user, since socket channel and engine are inner class' variables. + * {@link NioSslClient#write(String)} should be called instead. + * + * @param message - message to be sent to the server. + * @param engine - the engine used for encryption/decryption of the data exchanged between the two peers. + * @throws IOException if an I/O error occurs to the socket channel. + */ + @Override + protected void write(SocketChannel socketChannel, SSLEngine engine, String message) throws IOException { + + log.debug("About to write to the server..."); + + myAppData.clear(); + myAppData.put(message.getBytes()); + myAppData.flip(); + while (myAppData.hasRemaining()) { + // The loop has a meaning for (outgoing) messages larger than 16KB. + // Every wrap call will remove 16KB from the original message and send it to the remote peer. + myNetData.clear(); + SSLEngineResult result = engine.wrap(myAppData, myNetData); + switch (result.getStatus()) { + case OK: + myNetData.flip(); + while (myNetData.hasRemaining()) { + socketChannel.write(myNetData); + } + log.debug("Message sent to the server: " + message); + break; + case BUFFER_OVERFLOW: + myNetData = enlargePacketBuffer(engine, myNetData); + break; + case BUFFER_UNDERFLOW: + throw new SSLException("Buffer underflow occured after a wrap. I don't think we should ever get here."); + case CLOSED: + closeConnection(socketChannel, engine); + return; + default: + throw new IllegalStateException("Invalid SSL status: " + result.getStatus()); + } + } + + } + + /** + * Public method to try to read from the server. + */ + public void read() throws Exception { + read(socketChannel, engine); + } + + /** + * Will wait for response from the remote peer, until it actually gets something. + * Uses {@link SocketChannel#read(ByteBuffer)}, which is non-blocking, and if + * it gets nothing from the peer, waits for {@code waitToReadMillis} and tries again. + *