From 70f1ea0a9d0224312a3896df94389ae387995fb2 Mon Sep 17 00:00:00 2001 From: Christian Sailer Date: Fri, 22 May 2020 16:27:10 +0100 Subject: [PATCH] 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 }