diff --git a/node/src/integration-test/kotlin/net/corda/node/persistence/H2SecurityTests.kt b/node/src/integration-test/kotlin/net/corda/node/persistence/H2SecurityTests.kt index ba9e9cb1c5..0a8d8159d8 100644 --- a/node/src/integration-test/kotlin/net/corda/node/persistence/H2SecurityTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/persistence/H2SecurityTests.kt @@ -50,7 +50,8 @@ class H2SecurityTests { inMemoryDB = false, startNodesInProcess = false, notarySpecs = emptyList(), - cordappsForAllNodes = emptyList() + cordappsForAllNodes = emptyList(), + premigrateH2Database = false )) { val port = getFreePort() startNode(customOverrides = mapOf(h2AddressKey to "localhost:$port", dbPasswordKey to "x")).getOrThrow() @@ -71,7 +72,8 @@ class H2SecurityTests { inMemoryDB = false, startNodesInProcess = false, notarySpecs = emptyList(), - cordappsForAllNodes = listOf(enclosedCordapp()) + cordappsForAllNodes = listOf(enclosedCordapp()), + premigrateH2Database = false )) { val port = getFreePort() val nodeHandle = startNode(rpcUsers = listOf(user), customOverrides = mapOf(h2AddressKey to "localhost:$port", diff --git a/node/src/integration-test/kotlin/net/corda/node/services/statemachine/LargeTransactionsTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/statemachine/LargeTransactionsTest.kt index 6d23503efb..9e27f15d76 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/statemachine/LargeTransactionsTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/statemachine/LargeTransactionsTest.kt @@ -82,7 +82,8 @@ class LargeTransactionsTest { driver(DriverParameters( startNodesInProcess = true, cordappsForAllNodes = listOf(DUMMY_CONTRACTS_CORDAPP, enclosedCordapp()), - networkParameters = testNetworkParameters(maxMessageSize = 15.MB.toInt(), maxTransactionSize = 13.MB.toInt()) + networkParameters = testNetworkParameters(maxMessageSize = 15.MB.toInt(), maxTransactionSize = 13.MB.toInt()), + premigrateH2Database = false )) { val rpcUser = User("admin", "admin", setOf("ALL")) val (alice, _) = listOf(ALICE_NAME, BOB_NAME).map { startNode(providedName = it, rpcUsers = listOf(rpcUser)) }.transpose().getOrThrow() 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 87a1333545..cfd06a585b 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 @@ -43,21 +43,28 @@ data class NotaryHandle(val identity: Party, val validating: Boolean, val nodeHa interface NodeHandle : AutoCloseable { /** Get the [NodeInfo] for this node */ val nodeInfo: NodeInfo + /** * Interface to the node's RPC system. The first RPC user will be used to login if are any, otherwise a default one * will be added and that will be used. */ val rpc: CordaRPCOps + /** Get the p2p address for this node **/ val p2pAddress: NetworkHostAndPort + /** Get the rpc address for this node **/ val rpcAddress: NetworkHostAndPort + /** Get the rpc admin address for this node **/ val rpcAdminAddress: NetworkHostAndPort + /** Get the JMX server address for this node, if JMX is enabled **/ val jmxAddress: NetworkHostAndPort? + /** Get a [List] of [User]'s for this node **/ val rpcUsers: List + /** The location of the node's base directory **/ val baseDirectory: Path @@ -67,7 +74,8 @@ interface NodeHandle : AutoCloseable { fun stop() } -fun NodeHandle.logFile(): File = (baseDirectory / "logs").toFile().walk().filter { it.name.startsWith("node-") && it.extension == "log" }.single() +fun NodeHandle.logFile(): File = (baseDirectory / "logs").toFile().walk().filter { it.name.startsWith("node-") && it.extension == "log" } + .single() /** Interface which represents an out of process node and exposes its process handle. **/ @DoNotImplement @@ -91,7 +99,8 @@ interface InProcess : NodeHandle { * Starts an already constructed flow. Note that you must be on the server thread to call this method. * @param context indicates who started the flow, see: [InvocationContext]. */ - fun startFlow(logic: FlowLogic): CordaFuture = internalServices.startFlow(logic, internalServices.newContext()).getOrThrow().resultFuture + fun startFlow(logic: FlowLogic): CordaFuture = internalServices.startFlow(logic, internalServices.newContext()) + .getOrThrow().resultFuture } /** @@ -206,7 +215,8 @@ fun driver(defaultParameters: DriverParameters = DriverParameters(), dsl: Dr djvmBootstrapSource = defaultParameters.djvmBootstrapSource, djvmCordaSource = defaultParameters.djvmCordaSource, environmentVariables = defaultParameters.environmentVariables, - allowHibernateToManageAppSchema = defaultParameters.allowHibernateToManageAppSchema + allowHibernateToManageAppSchema = defaultParameters.allowHibernateToManageAppSchema, + premigrateH2Database = defaultParameters.premigrateH2Database ), coerce = { it }, dsl = dsl @@ -245,6 +255,8 @@ fun driver(defaultParameters: DriverParameters = DriverParameters(), dsl: Dr * @property cordappsForAllNodes [TestCordapp]s that will be added to each node started by the [DriverDSL]. * @property djvmBootstrapSource Location of a JAR containing the Java APIs for the DJVM to use. * @property djvmCordaSource Locations of JARs of user-supplied classes to execute within the DJVM sandbox. + * @property premigrateH2Database Whether to use a prebuilt H2 database schema or start from an empty schema. + * This can save time for tests which do not need to migrate from a blank schema. */ @Suppress("unused") data class DriverParameters( @@ -263,12 +275,13 @@ data class DriverParameters( @Suppress("DEPRECATION") val jmxPolicy: JmxPolicy = JmxPolicy(), val networkParameters: NetworkParameters = testNetworkParameters(notaries = emptyList()), val notaryCustomOverrides: Map = emptyMap(), - val inMemoryDB: Boolean = true, + val inMemoryDB: Boolean = false, val cordappsForAllNodes: Collection? = null, val djvmBootstrapSource: Path? = null, val djvmCordaSource: List = emptyList(), - val environmentVariables : Map = emptyMap(), - val allowHibernateToManageAppSchema: Boolean = true + val environmentVariables: Map = emptyMap(), + val allowHibernateToManageAppSchema: Boolean = true, + val premigrateH2Database: Boolean = true ) { constructor(cordappsForAllNodes: Collection) : this(isDebug = false, cordappsForAllNodes = cordappsForAllNodes) @@ -376,6 +389,49 @@ data class DriverParameters( cordappsForAllNodes = null ) + constructor( + isDebug: Boolean = false, + driverDirectory: Path = Paths.get("build") / "node-driver" / getTimestampAsDirectoryName(), + portAllocation: PortAllocation = incrementalPortAllocation(), + debugPortAllocation: PortAllocation = incrementalPortAllocation(), + systemProperties: Map = emptyMap(), + useTestClock: Boolean = false, + startNodesInProcess: Boolean = false, + waitForAllNodesToFinish: Boolean = false, + notarySpecs: List = listOf(NotarySpec(DUMMY_NOTARY_NAME)), + extraCordappPackagesToScan: List = emptyList(), + @Suppress("DEPRECATION") jmxPolicy: JmxPolicy = JmxPolicy(), + networkParameters: NetworkParameters = testNetworkParameters(notaries = emptyList()), + notaryCustomOverrides: Map = emptyMap(), + inMemoryDB: Boolean = false, + cordappsForAllNodes: Collection? = null, + djvmBootstrapSource: Path? = null, + djvmCordaSource: List = emptyList(), + environmentVariables: Map = emptyMap(), + allowHibernateToManageAppSchema: Boolean = true + ) : this( + isDebug, + driverDirectory, + portAllocation, + debugPortAllocation, + systemProperties, + useTestClock, + startNodesInProcess, + waitForAllNodesToFinish, + notarySpecs, + extraCordappPackagesToScan, + jmxPolicy, + networkParameters, + notaryCustomOverrides, + inMemoryDB, + cordappsForAllNodes, + djvmBootstrapSource, + djvmCordaSource, + environmentVariables, + allowHibernateToManageAppSchema, + premigrateH2Database = true + ) + constructor( isDebug: Boolean, driverDirectory: Path, @@ -417,6 +473,7 @@ data class DriverParameters( fun withStartNodesInProcess(startNodesInProcess: Boolean): DriverParameters = copy(startNodesInProcess = startNodesInProcess) fun withWaitForAllNodesToFinish(waitForAllNodesToFinish: Boolean): DriverParameters = copy(waitForAllNodesToFinish = waitForAllNodesToFinish) fun withNotarySpecs(notarySpecs: List): DriverParameters = copy(notarySpecs = notarySpecs) + @Deprecated("extraCordappPackagesToScan does not preserve the original CorDapp's versioning and metadata, which may lead to " + "misleading results in tests. Use withCordappsForAllNodes instead.") fun withExtraCordappPackagesToScan(extraCordappPackagesToScan: List): DriverParameters = copy(extraCordappPackagesToScan = extraCordappPackagesToScan) @@ -428,7 +485,7 @@ data class DriverParameters( fun withCordappsForAllNodes(cordappsForAllNodes: Collection?): DriverParameters = copy(cordappsForAllNodes = cordappsForAllNodes) 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 withEnvironmentVariables(variables: Map): DriverParameters = copy(environmentVariables = variables) fun withAllowHibernateToManageAppSchema(value: Boolean): DriverParameters = copy(allowHibernateToManageAppSchema = value) fun copy( @@ -530,4 +587,48 @@ data class DriverParameters( djvmCordaSource = djvmCordaSource, environmentVariables = environmentVariables ) + + // Legacy copy() from v4.5 + @Suppress("LongParameterList") + fun copy(isDebug: Boolean, + driverDirectory: Path, + portAllocation: PortAllocation, + debugPortAllocation: PortAllocation, + systemProperties: Map, + useTestClock: Boolean, + startNodesInProcess: Boolean, + waitForAllNodesToFinish: Boolean, + notarySpecs: List, + extraCordappPackagesToScan: List, + @Suppress("DEPRECATION") jmxPolicy: JmxPolicy, + networkParameters: NetworkParameters, + notaryCustomOverrides: Map, + inMemoryDB: Boolean, + cordappsForAllNodes: Collection?, + djvmBootstrapSource: Path?, + djvmCordaSource: List, + environmentVariables: Map, + allowHibernateToManageAppSchema: Boolean + ) = this.copy( + isDebug = isDebug, + driverDirectory = driverDirectory, + portAllocation = portAllocation, + debugPortAllocation = debugPortAllocation, + systemProperties = systemProperties, + useTestClock = useTestClock, + startNodesInProcess = startNodesInProcess, + waitForAllNodesToFinish = waitForAllNodesToFinish, + notarySpecs = notarySpecs, + extraCordappPackagesToScan = extraCordappPackagesToScan, + jmxPolicy = jmxPolicy, + networkParameters = networkParameters, + notaryCustomOverrides = notaryCustomOverrides, + inMemoryDB = inMemoryDB, + cordappsForAllNodes = cordappsForAllNodes, + djvmBootstrapSource = djvmBootstrapSource, + djvmCordaSource = djvmCordaSource, + environmentVariables = environmentVariables, + allowHibernateToManageAppSchema = allowHibernateToManageAppSchema, + premigrateH2Database = true + ) } \ No newline at end of file diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/DatabaseSnapshot.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/DatabaseSnapshot.kt new file mode 100644 index 0000000000..bc1d7f7e90 --- /dev/null +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/DatabaseSnapshot.kt @@ -0,0 +1,23 @@ +package net.corda.testing.node + +import java.io.InputStream +import java.nio.file.Files +import java.nio.file.Path + +object DatabaseSnapshot { + private const val previousCordaVersion: String = "4.5.1" + private const val databaseName: String = "persistence.mv.db" + + private fun getDatabaseSnapshotStream(): InputStream { + val resourceUri = this::class.java.getResource("/databasesnapshots/${previousCordaVersion}/$databaseName") + return resourceUri.openStream() + } + + fun copyDatabaseSnapshot(baseDirectory: Path) { + getDatabaseSnapshotStream().use { stream -> + Files.createDirectories(baseDirectory) + val path = baseDirectory.resolve(databaseName) + Files.copy(stream, path) + } + } +} \ No newline at end of file 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 5d9751ae5d..625d4f18be 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 @@ -48,6 +48,7 @@ import net.corda.testing.internal.configureDatabase import net.corda.testing.node.internal.* import net.corda.testing.services.MockAttachmentStorage import java.io.ByteArrayOutputStream +import java.nio.file.Paths import java.security.KeyPair import java.sql.Connection import java.time.Clock @@ -99,9 +100,17 @@ open class MockServices private constructor( // TODO: Can we use an X509 principal generator here? @JvmStatic fun makeTestDataSourceProperties(nodeName: String = SecureHash.randomSHA256().toString()): Properties { + val dbDir = Paths.get("","build", "mocknetworktestdb", nodeName) + .toAbsolutePath() + val dbPath = dbDir.resolve("persistence") + try { + DatabaseSnapshot.copyDatabaseSnapshot(dbDir) + } catch (ex: java.nio.file.FileAlreadyExistsException) { + DriverDSLImpl.log.warn("Database already exists on disk, not attempting to pre-migrate database.") + } val props = Properties() props.setProperty("dataSourceClassName", "org.h2.jdbcx.JdbcDataSource") - props.setProperty("dataSource.url", "jdbc:h2:mem:${nodeName}_persistence;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE") + props.setProperty("dataSource.url", "jdbc:h2:file:$dbPath;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE") props.setProperty("dataSource.user", "sa") props.setProperty("dataSource.password", "") return props @@ -357,7 +366,6 @@ open class MockServices private constructor( constructor(cordappPackages: List, initialIdentityName: CordaX500Name, identityService: IdentityService, networkParameters: NetworkParameters) : this(cordappPackages, TestIdentity(initialIdentityName), identityService, networkParameters) - constructor(cordappPackages: List, initialIdentityName: CordaX500Name, identityService: IdentityService, networkParameters: NetworkParameters, key: KeyPair) : this(cordappPackages, TestIdentity(initialIdentityName, key), identityService, networkParameters) @@ -428,11 +436,12 @@ open class MockServices private constructor( private val mockCordappProvider: MockCordappProvider = MockCordappProvider(cordappLoader, attachments).also { it.start() } - override val transactionVerifierService: TransactionVerifierService get() = InMemoryTransactionVerifierService( - numberOfWorkers = 2, - cordappProvider = mockCordappProvider, - attachments = attachments - ) + override val transactionVerifierService: TransactionVerifierService + get() = InMemoryTransactionVerifierService( + numberOfWorkers = 2, + cordappProvider = mockCordappProvider, + attachments = attachments + ) override val cordappProvider: CordappProvider get() = mockCordappProvider override var networkParametersService: NetworkParametersService = MockNetworkParametersStorage(initialNetworkParameters) override val diagnosticsService: DiagnosticsService = NodeDiagnosticsService() 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 7dc567f54a..f97f7836ae 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 @@ -53,6 +53,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.core.utilities.toHexString import net.corda.coretesting.internal.stubs.CertificateStoreStubs import net.corda.node.NodeRegistrationOption import net.corda.node.VersionInfo @@ -97,6 +98,7 @@ import net.corda.testing.driver.internal.InProcessImpl import net.corda.testing.driver.internal.NodeHandleInternal import net.corda.testing.driver.internal.OutOfProcessImpl import net.corda.testing.node.ClusterSpec +import net.corda.testing.node.DatabaseSnapshot import net.corda.testing.node.NotarySpec import okhttp3.OkHttpClient import okhttp3.Request @@ -149,7 +151,8 @@ class DriverDSLImpl( val djvmBootstrapSource: Path?, val djvmCordaSource: List, val environmentVariables: Map, - val allowHibernateToManageAppSchema: Boolean = true + val allowHibernateToManageAppSchema: Boolean = true, + val premigrateH2Database: Boolean = true ) : InternalDriverDSL { private var _executorService: ScheduledExecutorService? = null @@ -195,7 +198,7 @@ class DriverDSLImpl( } private fun NodeConfig.checkAndOverrideForInMemoryDB(): NodeConfig = this.run { - if (inMemoryDB && corda.dataSourceProperties.getProperty("dataSource.url").startsWith("jdbc:h2:")) { + if (inMemoryDB && isH2Database(corda)) { val jdbcUrl = "jdbc:h2:mem:persistence${inMemoryCounter.getAndIncrement()};DB_CLOSE_ON_EXIT=FALSE;LOCK_TIMEOUT=10000;WRITE_DELAY=100" corda.dataSourceProperties.setProperty("dataSource.url", jdbcUrl) NodeConfig(typesafe + mapOf("dataSourceProperties" to mapOf("dataSource.url" to jdbcUrl))) @@ -269,6 +272,15 @@ class DriverDSLImpl( val name = parameters.providedName ?: CordaX500Name("${oneOf(names).organisation}-${p2pAddress.port}", "London", "GB") val config = createConfig(name, parameters, p2pAddress) + if (premigrateH2Database && isH2Database(config)) { + if (!inMemoryDB) { + try { + DatabaseSnapshot.copyDatabaseSnapshot(config.corda.baseDirectory) + } catch (ex: java.nio.file.FileAlreadyExistsException) { + log.warn("Database already exists on disk, not attempting to pre-migrate database.") + } + } + } 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).flatMap { startNodeRegistration(it, compatibilityZone.rootCert, compatibilityZone.config()) } @@ -1032,6 +1044,12 @@ class DriverDSLImpl( ) } + private fun isH2Database(config: NodeConfiguration) + = config.dataSourceProperties.getProperty("dataSource.url").startsWith("jdbc:h2:") + + private fun isH2Database(config: NodeConfig) + = isH2Database(config.corda) + // Obvious test artifacts. This is NOT intended to be an exhaustive list! // It is only intended to remove those FEW jars which BLATANTLY do not // belong inside a Corda Node. @@ -1298,7 +1316,8 @@ fun genericDriver( djvmBootstrapSource = defaultParameters.djvmBootstrapSource, djvmCordaSource = defaultParameters.djvmCordaSource, environmentVariables = defaultParameters.environmentVariables, - allowHibernateToManageAppSchema = defaultParameters.allowHibernateToManageAppSchema + allowHibernateToManageAppSchema = defaultParameters.allowHibernateToManageAppSchema, + premigrateH2Database = defaultParameters.premigrateH2Database ) ) val shutdownHook = addShutdownHook(driverDsl::shutdown) @@ -1397,6 +1416,7 @@ fun internalDriver( djvmCordaSource: List = emptyList(), environmentVariables: Map = emptyMap(), allowHibernateToManageAppSchema: Boolean = true, + premigrateH2Database: Boolean = true, dsl: DriverDSLImpl.() -> A ): A { return genericDriver( @@ -1420,15 +1440,21 @@ fun internalDriver( djvmBootstrapSource = djvmBootstrapSource, djvmCordaSource = djvmCordaSource, environmentVariables = environmentVariables, - allowHibernateToManageAppSchema = allowHibernateToManageAppSchema + allowHibernateToManageAppSchema = allowHibernateToManageAppSchema, + premigrateH2Database = premigrateH2Database ), coerce = { it }, dsl = dsl ) } +val DIRECTORY_TIMESTAMP_FORMAT: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss.SSS").withZone(UTC) +private val directoryRandom = Random() fun getTimestampAsDirectoryName(): String { - return DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss.SSS").withZone(UTC).format(Instant.now()) + val base = DIRECTORY_TIMESTAMP_FORMAT.format(Instant.now()) + // Introduce some randomness so starting two nodes in the same ms doesn't use the same path + val random = directoryRandom.nextLong().toBigInteger().toByteArray().toHexString() + return "$base-$random" } fun writeConfig(path: Path, filename: String, config: Config) { diff --git a/testing/node-driver/src/main/resources/databasesnapshots/4.5.1/persistence.mv.db b/testing/node-driver/src/main/resources/databasesnapshots/4.5.1/persistence.mv.db new file mode 100644 index 0000000000..d5f6cdd09f Binary files /dev/null and b/testing/node-driver/src/main/resources/databasesnapshots/4.5.1/persistence.mv.db differ