Cleaned up Driver.kt so that only the relevant bits are exposed as public API

This commit is contained in:
Shams Asari 2017-12-02 11:19:18 +00:00
parent b0ebf3d7e0
commit 8461837f1a
27 changed files with 1138 additions and 1166 deletions

View File

@ -13,7 +13,7 @@ import net.corda.core.utilities.*
import net.corda.node.services.messaging.RPCServerConfiguration import net.corda.node.services.messaging.RPCServerConfiguration
import net.corda.nodeapi.RPCApi import net.corda.nodeapi.RPCApi
import net.corda.testing.SerializationEnvironmentRule import net.corda.testing.SerializationEnvironmentRule
import net.corda.testing.driver.poll import net.corda.testing.internal.poll
import net.corda.testing.internal.* import net.corda.testing.internal.*
import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.SimpleString
import org.junit.After import org.junit.After
@ -231,6 +231,7 @@ class RPCStabilityTests {
override val protocolVersion = 0 override val protocolVersion = 0
override fun ping() = "pong" override fun ping() = "pong"
} }
val serverFollower = shutdownManager.follower() val serverFollower = shutdownManager.follower()
val serverPort = startRpcServer<ReconnectOps>(ops = ops).getOrThrow().broker.hostAndPort!! val serverPort = startRpcServer<ReconnectOps>(ops = ops).getOrThrow().broker.hostAndPort!!
serverFollower.unfollow() serverFollower.unfollow()
@ -348,7 +349,7 @@ class RPCStabilityTests {
} }
fun RPCDriverExposedDSLInterface.pollUntilClientNumber(server: RpcServerHandle, expected: Int) { fun RPCDriverDSL.pollUntilClientNumber(server: RpcServerHandle, expected: Int) {
pollUntilTrue("number of RPC clients to become $expected") { pollUntilTrue("number of RPC clients to become $expected") {
val clientAddresses = server.broker.serverControl.addressNames.filter { it.startsWith(RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX) } val clientAddresses = server.broker.serverControl.addressNames.filter { it.startsWith(RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX) }
clientAddresses.size == expected clientAddresses.size == expected

View File

@ -7,7 +7,7 @@ import net.corda.core.messaging.RPCOps
import net.corda.node.services.messaging.RPCServerConfiguration import net.corda.node.services.messaging.RPCServerConfiguration
import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.config.User
import net.corda.testing.SerializationEnvironmentRule import net.corda.testing.SerializationEnvironmentRule
import net.corda.testing.internal.RPCDriverExposedDSLInterface import net.corda.testing.internal.RPCDriverDSL
import net.corda.testing.internal.rpcTestUser import net.corda.testing.internal.rpcTestUser
import net.corda.testing.internal.startInVmRpcClient import net.corda.testing.internal.startInVmRpcClient
import net.corda.testing.internal.startRpcClient import net.corda.testing.internal.startRpcClient
@ -41,7 +41,7 @@ open class AbstractRPCTest {
val createSession: () -> ClientSession val createSession: () -> ClientSession
) )
inline fun <reified I : RPCOps> RPCDriverExposedDSLInterface.testProxy( inline fun <reified I : RPCOps> RPCDriverDSL.testProxy(
ops: I, ops: I,
rpcUser: User = rpcTestUser, rpcUser: User = rpcTestUser,
clientConfiguration: RPCClientConfiguration = RPCClientConfiguration.default, clientConfiguration: RPCClientConfiguration = RPCClientConfiguration.default,
@ -55,9 +55,9 @@ open class AbstractRPCTest {
} }
} }
RPCTestMode.Netty -> RPCTestMode.Netty ->
startRpcServer(ops = ops, rpcUser = rpcUser, configuration = serverConfiguration).flatMap { server -> startRpcServer(ops = ops, rpcUser = rpcUser, configuration = serverConfiguration).flatMap { (broker) ->
startRpcClient<I>(server.broker.hostAndPort!!, rpcUser.username, rpcUser.password, clientConfiguration).map { startRpcClient<I>(broker.hostAndPort!!, rpcUser.username, rpcUser.password, clientConfiguration).map {
TestProxy(it, { startArtemisSession(server.broker.hostAndPort!!, rpcUser.username, rpcUser.password) }) TestProxy(it, { startArtemisSession(broker.hostAndPort!!, rpcUser.username, rpcUser.password) })
} }
} }
}.get() }.get()

View File

@ -7,7 +7,7 @@ import net.corda.core.internal.concurrent.thenMatch
import net.corda.core.messaging.RPCOps import net.corda.core.messaging.RPCOps
import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.getOrThrow
import net.corda.node.services.messaging.rpcContext import net.corda.node.services.messaging.rpcContext
import net.corda.testing.internal.RPCDriverExposedDSLInterface import net.corda.testing.internal.RPCDriverDSL
import net.corda.testing.internal.rpcDriver import net.corda.testing.internal.rpcDriver
import net.corda.testing.internal.rpcTestUser import net.corda.testing.internal.rpcTestUser
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
@ -26,7 +26,7 @@ import kotlin.test.assertTrue
class ClientRPCInfrastructureTests : AbstractRPCTest() { class ClientRPCInfrastructureTests : AbstractRPCTest() {
// TODO: Test that timeouts work // TODO: Test that timeouts work
private fun RPCDriverExposedDSLInterface.testProxy(): TestOps { private fun RPCDriverDSL.testProxy(): TestOps {
return testProxy<TestOps>(TestOpsImpl()).ops return testProxy<TestOps>(TestOpsImpl()).ops
} }

View File

@ -1,15 +1,15 @@
package net.corda.client.rpc package net.corda.client.rpc
import net.corda.client.rpc.internal.RPCClientConfiguration import net.corda.client.rpc.internal.RPCClientConfiguration
import net.corda.core.messaging.RPCOps
import net.corda.core.utilities.millis
import net.corda.core.crypto.random63BitValue import net.corda.core.crypto.random63BitValue
import net.corda.core.internal.concurrent.fork import net.corda.core.internal.concurrent.fork
import net.corda.core.internal.concurrent.transpose import net.corda.core.internal.concurrent.transpose
import net.corda.core.messaging.RPCOps
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.millis
import net.corda.node.services.messaging.RPCServerConfiguration import net.corda.node.services.messaging.RPCServerConfiguration
import net.corda.testing.internal.RPCDriverExposedDSLInterface import net.corda.testing.internal.RPCDriverDSL
import net.corda.testing.internal.rpcDriver import net.corda.testing.internal.rpcDriver
import net.corda.testing.internal.testThreadFactory import net.corda.testing.internal.testThreadFactory
import org.apache.activemq.artemis.utils.collections.ConcurrentHashSet import org.apache.activemq.artemis.utils.collections.ConcurrentHashSet
@ -20,7 +20,10 @@ import org.junit.runners.Parameterized
import rx.Observable import rx.Observable
import rx.subjects.UnicastSubject import rx.subjects.UnicastSubject
import java.util.* import java.util.*
import java.util.concurrent.* import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CountDownLatch
import java.util.concurrent.Executor
import java.util.concurrent.Executors
@RunWith(Parameterized::class) @RunWith(Parameterized::class)
class RPCConcurrencyTests : AbstractRPCTest() { class RPCConcurrencyTests : AbstractRPCTest() {
@ -84,7 +87,7 @@ class RPCConcurrencyTests : AbstractRPCTest() {
} }
} }
private fun RPCDriverExposedDSLInterface.testProxy(): TestProxy<TestOps> { private fun RPCDriverDSL.testProxy(): TestProxy<TestOps> {
return testProxy<TestOps>( return testProxy<TestOps>(
TestOpsImpl(pool), TestOpsImpl(pool),
clientConfiguration = RPCClientConfiguration.default.copy( clientConfiguration = RPCClientConfiguration.default.copy(

View File

@ -5,14 +5,14 @@ import net.corda.client.rpc.internal.RPCClientConfiguration
import net.corda.core.messaging.RPCOps import net.corda.core.messaging.RPCOps
import net.corda.core.utilities.minutes import net.corda.core.utilities.minutes
import net.corda.core.utilities.seconds import net.corda.core.utilities.seconds
import net.corda.testing.internal.performance.div
import net.corda.node.services.messaging.RPCServerConfiguration import net.corda.node.services.messaging.RPCServerConfiguration
import net.corda.testing.internal.RPCDriverExposedDSLInterface import net.corda.testing.internal.RPCDriverDSL
import net.corda.testing.measure import net.corda.testing.internal.performance.div
import net.corda.testing.internal.performance.startPublishingFixedRateInjector import net.corda.testing.internal.performance.startPublishingFixedRateInjector
import net.corda.testing.internal.performance.startReporter import net.corda.testing.internal.performance.startReporter
import net.corda.testing.internal.performance.startTightLoopInjector import net.corda.testing.internal.performance.startTightLoopInjector
import net.corda.testing.internal.rpcDriver import net.corda.testing.internal.rpcDriver
import net.corda.testing.measure
import org.junit.Ignore import org.junit.Ignore
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@ -42,7 +42,7 @@ class RPCPerformanceTests : AbstractRPCTest() {
} }
} }
private fun RPCDriverExposedDSLInterface.testProxy( private fun RPCDriverDSL.testProxy(
clientConfiguration: RPCClientConfiguration, clientConfiguration: RPCClientConfiguration,
serverConfiguration: RPCServerConfiguration serverConfiguration: RPCServerConfiguration
): TestProxy<TestOps> { ): TestProxy<TestOps> {

View File

@ -5,7 +5,7 @@ import net.corda.core.messaging.RPCOps
import net.corda.node.services.Permissions.Companion.invokeRpc import net.corda.node.services.Permissions.Companion.invokeRpc
import net.corda.node.services.messaging.rpcContext import net.corda.node.services.messaging.rpcContext
import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.config.User
import net.corda.testing.internal.RPCDriverExposedDSLInterface import net.corda.testing.internal.RPCDriverDSL
import net.corda.testing.internal.rpcDriver import net.corda.testing.internal.rpcDriver
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@ -37,7 +37,7 @@ class RPCPermissionsTests : AbstractRPCTest() {
/** /**
* Create an RPC proxy for the given user. * Create an RPC proxy for the given user.
*/ */
private fun RPCDriverExposedDSLInterface.testProxyFor(rpcUser: User) = testProxy<TestOps>(TestOpsImpl(), rpcUser).ops private fun RPCDriverDSL.testProxyFor(rpcUser: User) = testProxy<TestOps>(TestOpsImpl(), rpcUser).ops
private fun userOf(name: String, permissions: Set<String>) = User(name, "password", permissions) private fun userOf(name: String, permissions: Set<String>) = User(name, "password", permissions)

View File

@ -20,14 +20,17 @@ import net.corda.node.internal.SecureCordaRPCOps
import net.corda.node.internal.StartedNode import net.corda.node.internal.StartedNode
import net.corda.node.services.Permissions.Companion.startFlow import net.corda.node.services.Permissions.Companion.startFlow
import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.config.User
import net.corda.testing.* import net.corda.testing.ALICE_NAME
import net.corda.testing.BOB_NAME
import net.corda.testing.contracts.DummyContract import net.corda.testing.contracts.DummyContract
import net.corda.testing.contracts.DummyContractV2 import net.corda.testing.contracts.DummyContractV2
import net.corda.testing.internal.RPCDriverExposedDSLInterface import net.corda.testing.internal.RPCDriverDSL
import net.corda.testing.internal.rpcDriver import net.corda.testing.internal.rpcDriver
import net.corda.testing.internal.rpcTestUser import net.corda.testing.internal.rpcTestUser
import net.corda.testing.internal.startRpcClient import net.corda.testing.internal.startRpcClient
import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNetwork
import net.corda.testing.singleIdentity
import net.corda.testing.startFlow
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
@ -120,7 +123,7 @@ class ContractUpgradeFlowTest {
check(bobNode) check(bobNode)
} }
private fun RPCDriverExposedDSLInterface.startProxy(node: StartedNode<*>, user: User): CordaRPCOps { private fun RPCDriverDSL.startProxy(node: StartedNode<*>, user: User): CordaRPCOps {
return startRpcClient<CordaRPCOps>( return startRpcClient<CordaRPCOps>(
rpcAddress = startRpcServer( rpcAddress = startRpcServer(
rpcUser = user, rpcUser = user,

View File

@ -17,11 +17,12 @@ import net.corda.nodeapi.internal.config.User
import net.corda.testing.DUMMY_NOTARY import net.corda.testing.DUMMY_NOTARY
import net.corda.testing.driver.NodeHandle import net.corda.testing.driver.NodeHandle
import net.corda.testing.driver.driver import net.corda.testing.driver.driver
import net.corda.testing.node.NotarySpec import net.corda.testing.internal.InternalDriverDSL
import net.corda.testing.internal.performance.div import net.corda.testing.internal.performance.div
import net.corda.testing.internal.performance.startPublishingFixedRateInjector import net.corda.testing.internal.performance.startPublishingFixedRateInjector
import net.corda.testing.internal.performance.startReporter import net.corda.testing.internal.performance.startReporter
import net.corda.testing.internal.performance.startTightLoopInjector import net.corda.testing.internal.performance.startTightLoopInjector
import net.corda.testing.node.NotarySpec
import org.junit.Before import org.junit.Before
import org.junit.Ignore import org.junit.Ignore
import org.junit.Test import org.junit.Test
@ -91,7 +92,7 @@ class NodePerformanceTests {
driver(startNodesInProcess = true) { driver(startNodesInProcess = true) {
val a = startNode(rpcUsers = listOf(User("A", "A", setOf(startFlow<EmptyFlow>())))).get() val a = startNode(rpcUsers = listOf(User("A", "A", setOf(startFlow<EmptyFlow>())))).get()
a as NodeHandle.InProcess a as NodeHandle.InProcess
val metricRegistry = startReporter(shutdownManager, a.node.services.monitoringService.metrics) val metricRegistry = startReporter((this as InternalDriverDSL).shutdownManager, a.node.services.monitoringService.metrics)
a.rpcClientToNode().use("A", "A") { connection -> a.rpcClientToNode().use("A", "A") { connection ->
startPublishingFixedRateInjector(metricRegistry, 8, 5.minutes, 2000L / TimeUnit.SECONDS) { startPublishingFixedRateInjector(metricRegistry, 8, 5.minutes, 2000L / TimeUnit.SECONDS) {
connection.proxy.startFlow(::EmptyFlow).returnValue.get() connection.proxy.startFlow(::EmptyFlow).returnValue.get()
@ -109,7 +110,7 @@ class NodePerformanceTests {
extraCordappPackagesToScan = listOf("net.corda.finance") extraCordappPackagesToScan = listOf("net.corda.finance")
) { ) {
val notary = defaultNotaryNode.getOrThrow() as NodeHandle.InProcess val notary = defaultNotaryNode.getOrThrow() as NodeHandle.InProcess
val metricRegistry = startReporter(shutdownManager, notary.node.services.monitoringService.metrics) val metricRegistry = startReporter((this as InternalDriverDSL).shutdownManager, notary.node.services.monitoringService.metrics)
notary.rpcClientToNode().use("A", "A") { connection -> notary.rpcClientToNode().use("A", "A") { connection ->
println("ISSUING") println("ISSUING")
val doneFutures = (1..100).toList().parallelStream().map { val doneFutures = (1..100).toList().parallelStream().map {

View File

@ -20,13 +20,14 @@ import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.getOrThrow
import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.internal.cordapp.CordappLoader
import net.corda.node.internal.cordapp.CordappProviderImpl import net.corda.node.internal.cordapp.CordappProviderImpl
import net.corda.testing.*
import net.corda.testing.DUMMY_BANK_A import net.corda.testing.DUMMY_BANK_A
import net.corda.testing.DUMMY_NOTARY import net.corda.testing.DUMMY_NOTARY
import net.corda.testing.driver.DriverDSLExposedInterface import net.corda.testing.driver.DriverDSL
import net.corda.testing.driver.NodeHandle import net.corda.testing.driver.NodeHandle
import net.corda.testing.driver.driver import net.corda.testing.driver.driver
import net.corda.testing.node.MockAttachmentStorage import net.corda.testing.node.MockAttachmentStorage
import net.corda.testing.rigorousMock
import net.corda.testing.withTestSerialization
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
import java.net.URLClassLoader import java.net.URLClassLoader
@ -51,16 +52,16 @@ class AttachmentLoadingTests {
Class.forName("net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Initiator", true, URLClassLoader(arrayOf(isolatedJAR))) Class.forName("net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Initiator", true, URLClassLoader(arrayOf(isolatedJAR)))
.asSubclass(FlowLogic::class.java) .asSubclass(FlowLogic::class.java)
private fun DriverDSLExposedInterface.createTwoNodes(): List<NodeHandle> { private fun DriverDSL.createTwoNodes(): List<NodeHandle> {
return listOf( return listOf(
startNode(providedName = bankAName), startNode(providedName = bankAName),
startNode(providedName = bankBName) startNode(providedName = bankBName)
).transpose().getOrThrow() ).transpose().getOrThrow()
} }
private fun DriverDSLExposedInterface.installIsolatedCordappTo(nodeName: CordaX500Name) { private fun DriverDSL.installIsolatedCordappTo(nodeName: CordaX500Name) {
// Copy the app jar to the first node. The second won't have it. // Copy the app jar to the first node. The second won't have it.
val path = (baseDirectory(nodeName.toString()) / "cordapps").createDirectories() / "isolated.jar" val path = (baseDirectory(nodeName) / "cordapps").createDirectories() / "isolated.jar"
logger.info("Installing isolated jar to $path") logger.info("Installing isolated jar to $path")
isolatedJAR.openStream().buffered().use { input -> isolatedJAR.openStream().buffered().use { input ->
Files.newOutputStream(path).buffered().use { output -> Files.newOutputStream(path).buffered().use { output ->

View File

@ -18,7 +18,7 @@ import net.corda.node.services.messaging.*
import net.corda.node.services.transactions.RaftValidatingNotaryService import net.corda.node.services.transactions.RaftValidatingNotaryService
import net.corda.testing.ALICE import net.corda.testing.ALICE
import net.corda.testing.chooseIdentity import net.corda.testing.chooseIdentity
import net.corda.testing.driver.DriverDSLExposedInterface import net.corda.testing.driver.DriverDSL
import net.corda.testing.driver.NodeHandle import net.corda.testing.driver.NodeHandle
import net.corda.testing.driver.driver import net.corda.testing.driver.driver
import net.corda.testing.node.ClusterSpec import net.corda.testing.node.ClusterSpec
@ -116,13 +116,13 @@ class P2PMessagingTest {
} }
} }
private fun startDriverWithDistributedService(dsl: DriverDSLExposedInterface.(List<StartedNode<Node>>) -> Unit) { private fun startDriverWithDistributedService(dsl: DriverDSL.(List<StartedNode<Node>>) -> Unit) {
driver(startNodesInProcess = true, notarySpecs = listOf(NotarySpec(DISTRIBUTED_SERVICE_NAME, cluster = ClusterSpec.Raft(clusterSize = 2)))) { driver(startNodesInProcess = true, notarySpecs = listOf(NotarySpec(DISTRIBUTED_SERVICE_NAME, cluster = ClusterSpec.Raft(clusterSize = 2)))) {
dsl(defaultNotaryHandle.nodeHandles.getOrThrow().map { (it as NodeHandle.InProcess).node }) dsl(defaultNotaryHandle.nodeHandles.getOrThrow().map { (it as NodeHandle.InProcess).node })
} }
} }
private fun DriverDSLExposedInterface.startAlice(): StartedNode<Node> { private fun DriverDSL.startAlice(): StartedNode<Node> {
return startNode(providedName = ALICE.name, customOverrides = mapOf("messageRedeliveryDelaySeconds" to 1)) return startNode(providedName = ALICE.name, customOverrides = mapOf("messageRedeliveryDelaySeconds" to 1))
.map { (it as NodeHandle.InProcess).node } .map { (it as NodeHandle.InProcess).node }
.getOrThrow() .getOrThrow()

View File

@ -25,7 +25,7 @@ import net.corda.core.utilities.ProgressTracker
import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.getOrThrow
import net.corda.testing.DUMMY_BANK_B import net.corda.testing.DUMMY_BANK_B
import net.corda.testing.DUMMY_NOTARY import net.corda.testing.DUMMY_NOTARY
import net.corda.testing.driver.poll import net.corda.testing.internal.poll
import java.io.InputStream import java.io.InputStream
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.URL import java.net.URL

View File

@ -3,8 +3,11 @@ package net.corda.test.spring
import net.corda.core.concurrent.CordaFuture import net.corda.core.concurrent.CordaFuture
import net.corda.core.internal.concurrent.map import net.corda.core.internal.concurrent.map
import net.corda.core.utilities.contextLogger import net.corda.core.utilities.contextLogger
import net.corda.testing.driver.* import net.corda.testing.driver.DriverParameters
import net.corda.testing.internal.ProcessUtilities import net.corda.testing.driver.NodeHandle
import net.corda.testing.driver.PortAllocation
import net.corda.testing.driver.WebserverHandle
import net.corda.testing.internal.*
import net.corda.testing.node.NotarySpec import net.corda.testing.node.NotarySpec
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
@ -14,22 +17,6 @@ import java.nio.file.Path
import java.nio.file.Paths import java.nio.file.Paths
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
interface SpringDriverExposedDSLInterface : DriverDSLExposedInterface {
/**
* Starts a Spring Boot application, passes the RPC connection data as parameters the process.
* Returns future which will complete after (and if) the server passes healthcheck.
* @param clazz Class with main method which is expected to run Spring application
* @param handle Corda Node handle this webapp is expected to connect to
* @param checkUrl URL path to use for server readiness check - uses [okhttp3.Response.isSuccessful] as qualifier
*
* TODO: Rather then expecting a given clazz to contain main method which start Spring app our own simple class can do this
*/
fun startSpringBootWebapp(clazz: Class<*>, handle: NodeHandle, checkUrl: String): CordaFuture<WebserverHandle>
}
interface SpringDriverInternalDSLInterface : DriverDSLInternalInterface, SpringDriverExposedDSLInterface
fun <A> springDriver( fun <A> springDriver(
defaultParameters: DriverParameters = DriverParameters(), defaultParameters: DriverParameters = DriverParameters(),
isDebug: Boolean = defaultParameters.isDebug, isDebug: Boolean = defaultParameters.isDebug,
@ -42,29 +29,40 @@ fun <A> springDriver(
startNodesInProcess: Boolean = defaultParameters.startNodesInProcess, startNodesInProcess: Boolean = defaultParameters.startNodesInProcess,
notarySpecs: List<NotarySpec>, notarySpecs: List<NotarySpec>,
extraCordappPackagesToScan: List<String> = defaultParameters.extraCordappPackagesToScan, extraCordappPackagesToScan: List<String> = defaultParameters.extraCordappPackagesToScan,
dsl: SpringDriverExposedDSLInterface.() -> A dsl: SpringBootDriverDSL.() -> A
) = genericDriver( ): A {
defaultParameters = defaultParameters, return genericDriver(
isDebug = isDebug, defaultParameters = defaultParameters,
driverDirectory = driverDirectory, isDebug = isDebug,
portAllocation = portAllocation, driverDirectory = driverDirectory,
debugPortAllocation = debugPortAllocation, portAllocation = portAllocation,
systemProperties = systemProperties, debugPortAllocation = debugPortAllocation,
useTestClock = useTestClock, systemProperties = systemProperties,
initialiseSerialization = initialiseSerialization, useTestClock = useTestClock,
startNodesInProcess = startNodesInProcess, initialiseSerialization = initialiseSerialization,
extraCordappPackagesToScan = extraCordappPackagesToScan, startNodesInProcess = startNodesInProcess,
notarySpecs = notarySpecs, extraCordappPackagesToScan = extraCordappPackagesToScan,
driverDslWrapper = { driverDSL:DriverDSL -> SpringBootDriverDSL(driverDSL) }, notarySpecs = notarySpecs,
coerce = { it }, dsl = dsl driverDslWrapper = { driverDSL: DriverDSLImpl -> SpringBootDriverDSL(driverDSL) },
) coerce = { it }, dsl = dsl
)
}
data class SpringBootDriverDSL(private val driverDSL: DriverDSL) : DriverDSLInternalInterface by driverDSL, SpringDriverInternalDSLInterface { data class SpringBootDriverDSL(private val driverDSL: DriverDSLImpl) : InternalDriverDSL by driverDSL {
companion object { companion object {
private val log = contextLogger() private val log = contextLogger()
} }
override fun startSpringBootWebapp(clazz: Class<*>, handle: NodeHandle, checkUrl: String): CordaFuture<WebserverHandle> { /**
* Starts a Spring Boot application, passes the RPC connection data as parameters the process.
* Returns future which will complete after (and if) the server passes healthcheck.
* @param clazz Class with main method which is expected to run Spring application
* @param handle Corda Node handle this webapp is expected to connect to
* @param checkUrl URL path to use for server readiness check - uses [okhttp3.Response.isSuccessful] as qualifier
*
* TODO: Rather then expecting a given clazz to contain main method which start Spring app our own simple class can do this
*/
fun startSpringBootWebapp(clazz: Class<*>, handle: NodeHandle, checkUrl: String): CordaFuture<WebserverHandle> {
val debugPort = if (driverDSL.isDebug) driverDSL.debugPortAllocation.nextPort() else null val debugPort = if (driverDSL.isDebug) driverDSL.debugPortAllocation.nextPort() else null
val process = startApplication(handle, debugPort, clazz) val process = startApplication(handle, debugPort, clazz)
driverDSL.shutdownManager.registerProcessShutdown(process) driverDSL.shutdownManager.registerProcessShutdown(process)

View File

@ -15,7 +15,7 @@ import net.corda.testing.DUMMY_BANK_B
import net.corda.testing.chooseIdentity import net.corda.testing.chooseIdentity
import net.corda.testing.driver.NodeHandle import net.corda.testing.driver.NodeHandle
import net.corda.testing.driver.driver import net.corda.testing.driver.driver
import net.corda.testing.driver.poll import net.corda.testing.internal.poll
import net.corda.traderdemo.flow.BuyerFlow import net.corda.traderdemo.flow.BuyerFlow
import net.corda.traderdemo.flow.CommercialPaperIssueFlow import net.corda.traderdemo.flow.CommercialPaperIssueFlow
import net.corda.traderdemo.flow.SellerFlow import net.corda.traderdemo.flow.SellerFlow

View File

@ -10,6 +10,8 @@ import net.corda.testing.DUMMY_BANK_A
import net.corda.testing.DUMMY_NOTARY import net.corda.testing.DUMMY_NOTARY
import net.corda.testing.DUMMY_REGULATOR import net.corda.testing.DUMMY_REGULATOR
import net.corda.testing.common.internal.ProjectStructure.projectRootDir import net.corda.testing.common.internal.ProjectStructure.projectRootDir
import net.corda.testing.internal.addressMustBeBound
import net.corda.testing.internal.addressMustNotBeBound
import net.corda.testing.node.NotarySpec import net.corda.testing.node.NotarySpec
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.junit.Test import org.junit.Test
@ -74,7 +76,7 @@ class DriverTests {
} }
val baseDirectory = driver(notarySpecs = listOf(NotarySpec(DUMMY_NOTARY.name))) { val baseDirectory = driver(notarySpecs = listOf(NotarySpec(DUMMY_NOTARY.name))) {
(this as DriverDSL).baseDirectory(DUMMY_NOTARY.name) baseDirectory(DUMMY_NOTARY.name)
} }
assertThat(baseDirectory / "process-id").doesNotExist() assertThat(baseDirectory / "process-id").doesNotExist()
} }

View File

@ -2,202 +2,35 @@
package net.corda.testing.driver package net.corda.testing.driver
import com.google.common.util.concurrent.ThreadFactoryBuilder
import com.typesafe.config.Config
import com.typesafe.config.ConfigRenderOptions
import net.corda.client.rpc.CordaRPCClient import net.corda.client.rpc.CordaRPCClient
import net.corda.cordform.CordformContext
import net.corda.cordform.CordformNode
import net.corda.core.CordaException
import net.corda.core.concurrent.CordaFuture import net.corda.core.concurrent.CordaFuture
import net.corda.core.concurrent.firstOf
import net.corda.core.identity.CordaX500Name import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.internal.*
import net.corda.core.internal.concurrent.*
import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.CordaRPCOps
import net.corda.core.node.NodeInfo import net.corda.core.node.NodeInfo
import net.corda.core.node.services.NetworkMapCache import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.node.services.NotaryService
import net.corda.core.toFuture
import net.corda.core.utilities.*
import net.corda.node.internal.Node import net.corda.node.internal.Node
import net.corda.node.internal.NodeStartup
import net.corda.node.internal.StartedNode import net.corda.node.internal.StartedNode
import net.corda.node.services.Permissions.Companion.invokeRpc import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.config.* import net.corda.node.services.config.VerifierType
import net.corda.node.utilities.ServiceIdentityGenerator
import net.corda.nodeapi.NodeInfoFilesCopier
import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.config.User
import net.corda.nodeapi.internal.config.toConfig import net.corda.testing.DUMMY_NOTARY
import net.corda.nodeapi.internal.addShutdownHook
import net.corda.testing.*
import net.corda.testing.internal.InProcessNode import net.corda.testing.internal.InProcessNode
import net.corda.testing.internal.ProcessUtilities import net.corda.testing.internal.DriverDSLImpl
import net.corda.testing.node.ClusterSpec import net.corda.testing.internal.genericDriver
import net.corda.testing.node.MockServices.Companion.MOCK_VERSION_INFO import net.corda.testing.internal.getTimestampAsDirectoryName
import net.corda.testing.node.NotarySpec import net.corda.testing.node.NotarySpec
import okhttp3.OkHttpClient import java.net.InetSocketAddress
import okhttp3.Request import java.net.ServerSocket
import org.slf4j.Logger
import rx.Observable
import rx.observables.ConnectableObservable
import java.net.*
import java.nio.file.Path import java.nio.file.Path
import java.nio.file.Paths import java.nio.file.Paths
import java.nio.file.StandardCopyOption.REPLACE_EXISTING
import java.time.Duration
import java.time.Instant
import java.time.ZoneOffset.UTC
import java.time.format.DateTimeFormatter
import java.util.*
import java.util.concurrent.Executors
import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.TimeUnit.MILLISECONDS
import java.util.concurrent.TimeUnit.SECONDS
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import kotlin.concurrent.thread
/**
* This file defines a small "Driver" DSL for starting up nodes that is only intended for development, demos and tests.
*
* The process the driver is run in behaves as an Artemis client and starts up other processes.
*
* TODO this file is getting way too big, it should be split into several files.
*/
private val log: Logger = loggerFor<DriverDSL>()
private val DEFAULT_POLL_INTERVAL = 500.millis
private const val DEFAULT_WARN_COUNT = 120
/**
* A sub-set of permissions that grant most of the essential operations used in the unit/integration tests as well as
* in demo application like NodeExplorer.
*/
private val DRIVER_REQUIRED_PERMISSIONS = setOf(
invokeRpc(CordaRPCOps::nodeInfo),
invokeRpc(CordaRPCOps::networkMapFeed),
invokeRpc(CordaRPCOps::networkMapSnapshot),
invokeRpc(CordaRPCOps::notaryIdentities),
invokeRpc(CordaRPCOps::stateMachinesFeed),
invokeRpc(CordaRPCOps::stateMachineRecordedTransactionMappingFeed),
invokeRpc(CordaRPCOps::nodeInfoFromParty),
invokeRpc(CordaRPCOps::internalVerifiedTransactionsFeed),
invokeRpc("vaultQueryBy"),
invokeRpc("vaultTrackBy"),
invokeRpc(CordaRPCOps::registeredFlows)
)
/** /**
* Object ecapsulating a notary started automatically by the driver. * Object ecapsulating a notary started automatically by the driver.
*/ */
data class NotaryHandle(val identity: Party, val validating: Boolean, val nodeHandles: CordaFuture<List<NodeHandle>>) data class NotaryHandle(val identity: Party, val validating: Boolean, val nodeHandles: CordaFuture<List<NodeHandle>>)
/**
* This is the interface that's exposed to DSL users.
*/
interface DriverDSLExposedInterface : CordformContext {
/** Returns a list of [NotaryHandle]s matching the list of [NotarySpec]s passed into [driver]. */
val notaryHandles: List<NotaryHandle>
/**
* Returns the [NotaryHandle] for the single notary on the network. Throws if there are none or more than one.
* @see notaryHandles
*/
val defaultNotaryHandle: NotaryHandle get() {
return when (notaryHandles.size) {
0 -> throw IllegalStateException("There are no notaries defined on the network")
1 -> notaryHandles[0]
else -> throw IllegalStateException("There is more than one notary defined on the network")
}
}
/**
* Returns the identity of the single notary on the network. Throws if there are none or more than one.
* @see defaultNotaryHandle
*/
val defaultNotaryIdentity: Party get() = defaultNotaryHandle.identity
/**
* Returns a [CordaFuture] on the [NodeHandle] for the single-node notary on the network. Throws if there
* are no notaries or more than one, or if the notary is a distributed cluster.
* @see defaultNotaryHandle
* @see notaryHandles
*/
val defaultNotaryNode: CordaFuture<NodeHandle> get() {
return defaultNotaryHandle.nodeHandles.map {
it.singleOrNull() ?: throw IllegalStateException("Default notary is not a single node")
}
}
/**
* Start a node.
*
* @param defaultParameters The default parameters for the node. Allows the node to be configured in builder style
* when called from Java code.
* @param providedName Optional name of the node, which will be its legal name in [Party]. Defaults to something
* random. Note that this must be unique as the driver uses it as a primary key!
* @param verifierType The type of transaction verifier to use. See: [VerifierType]
* @param rpcUsers List of users who are authorised to use the RPC system. Defaults to empty list.
* @param startInSameProcess Determines if the node should be started inside the same process the Driver is running
* in. If null the Driver-level value will be used.
* @return A [CordaFuture] on the [NodeHandle] to the node. The future will complete when the node is available.
*/
fun startNode(
defaultParameters: NodeParameters = NodeParameters(),
providedName: CordaX500Name? = defaultParameters.providedName,
rpcUsers: List<User> = defaultParameters.rpcUsers,
verifierType: VerifierType = defaultParameters.verifierType,
customOverrides: Map<String, Any?> = defaultParameters.customOverrides,
startInSameProcess: Boolean? = defaultParameters.startInSameProcess,
maximumHeapSize: String = defaultParameters.maximumHeapSize): CordaFuture<NodeHandle>
/**
* Helper function for starting a [Node] with custom parameters from Java.
*
* @param parameters The default parameters for the driver.
* @return [NodeHandle] that will be available sometime in the future.
*/
fun startNode(parameters: NodeParameters): CordaFuture<NodeHandle> = startNode(defaultParameters = parameters)
/** Call [startWebserver] with a default maximumHeapSize. */
fun startWebserver(handle: NodeHandle): CordaFuture<WebserverHandle> = startWebserver(handle, "200m")
/**
* Starts a web server for a node
* @param handle The handle for the node that this webserver connects to via RPC.
* @param maximumHeapSize Argument for JVM -Xmx option e.g. "200m".
*/
fun startWebserver(handle: NodeHandle, maximumHeapSize: String): CordaFuture<WebserverHandle>
/**
* Polls a function until it returns a non-null value. Note that there is no timeout on the polling.
*
* @param pollName A description of what is being polled.
* @param pollInterval The interval of polling.
* @param warnCount The number of polls after the Driver gives a warning.
* @param check The function being polled.
* @return A future that completes with the non-null value [check] has returned.
*/
fun <A> pollUntilNonNull(pollName: String, pollInterval: Duration = DEFAULT_POLL_INTERVAL, warnCount: Int = DEFAULT_WARN_COUNT, check: () -> A?): CordaFuture<A>
/**
* Polls the given function until it returns true.
* @see pollUntilNonNull
*/
fun pollUntilTrue(pollName: String, pollInterval: Duration = DEFAULT_POLL_INTERVAL, warnCount: Int = DEFAULT_WARN_COUNT, check: () -> Boolean): CordaFuture<Unit> {
return pollUntilNonNull(pollName, pollInterval, warnCount) { if (check()) Unit else null }
}
val shutdownManager: ShutdownManager
}
interface DriverDSLInternalInterface : DriverDSLExposedInterface {
fun start()
fun shutdown()
}
sealed class NodeHandle { sealed class NodeHandle {
abstract val nodeInfo: NodeInfo abstract val nodeInfo: NodeInfo
/** /**
@ -319,9 +152,9 @@ data class NodeParameters(
* @param systemProperties A Map of extra system properties which will be given to each new node. Defaults to empty. * @param systemProperties A Map of extra system properties which will be given to each new node. Defaults to empty.
* @param useTestClock If true the test clock will be used in Node. * @param useTestClock If true the test clock will be used in Node.
* @param startNodesInProcess Provides the default behaviour of whether new nodes should start inside this process or * @param startNodesInProcess Provides the default behaviour of whether new nodes should start inside this process or
* not. Note that this may be overridden in [DriverDSLExposedInterface.startNode]. * not. Note that this may be overridden in [DriverDSL.startNode].
* @param notarySpecs The notaries advertised for this network. These nodes will be started automatically and will be * @param notarySpecs The notaries advertised for this network. These nodes will be started automatically and will be
* available from [DriverDSLExposedInterface.notaryHandles]. Defaults to a simple validating notary. * available from [DriverDSL.notaryHandles]. Defaults to a simple validating notary.
* @param dsl The dsl itself. * @param dsl The dsl itself.
* @return The value returned in the [dsl] closure. * @return The value returned in the [dsl] closure.
*/ */
@ -335,13 +168,13 @@ fun <A> driver(
useTestClock: Boolean = defaultParameters.useTestClock, useTestClock: Boolean = defaultParameters.useTestClock,
initialiseSerialization: Boolean = defaultParameters.initialiseSerialization, initialiseSerialization: Boolean = defaultParameters.initialiseSerialization,
startNodesInProcess: Boolean = defaultParameters.startNodesInProcess, startNodesInProcess: Boolean = defaultParameters.startNodesInProcess,
waitForAllNodesToFinish: Boolean = defaultParameters.waitForNodesToFinish, waitForAllNodesToFinish: Boolean = defaultParameters.waitForAllNodesToFinish,
notarySpecs: List<NotarySpec> = defaultParameters.notarySpecs, notarySpecs: List<NotarySpec> = defaultParameters.notarySpecs,
extraCordappPackagesToScan: List<String> = defaultParameters.extraCordappPackagesToScan, extraCordappPackagesToScan: List<String> = defaultParameters.extraCordappPackagesToScan,
dsl: DriverDSLExposedInterface.() -> A dsl: DriverDSL.() -> A
): A { ): A {
return genericDriver( return genericDriver(
driverDsl = DriverDSL( driverDsl = DriverDSLImpl(
portAllocation = portAllocation, portAllocation = portAllocation,
debugPortAllocation = debugPortAllocation, debugPortAllocation = debugPortAllocation,
systemProperties = systemProperties, systemProperties = systemProperties,
@ -368,7 +201,7 @@ fun <A> driver(
*/ */
fun <A> driver( fun <A> driver(
parameters: DriverParameters, parameters: DriverParameters,
dsl: DriverDSLExposedInterface.() -> A dsl: DriverDSL.() -> A
): A { ): A {
return driver(defaultParameters = parameters, dsl = dsl) return driver(defaultParameters = parameters, dsl = dsl)
} }
@ -384,7 +217,7 @@ data class DriverParameters(
val useTestClock: Boolean = false, val useTestClock: Boolean = false,
val initialiseSerialization: Boolean = true, val initialiseSerialization: Boolean = true,
val startNodesInProcess: Boolean = false, val startNodesInProcess: Boolean = false,
val waitForNodesToFinish: Boolean = false, val waitForAllNodesToFinish: Boolean = false,
val notarySpecs: List<NotarySpec> = listOf(NotarySpec(DUMMY_NOTARY.name)), val notarySpecs: List<NotarySpec> = listOf(NotarySpec(DUMMY_NOTARY.name)),
val extraCordappPackagesToScan: List<String> = emptyList() val extraCordappPackagesToScan: List<String> = emptyList()
) { ) {
@ -396,643 +229,7 @@ data class DriverParameters(
fun setUseTestClock(useTestClock: Boolean) = copy(useTestClock = useTestClock) fun setUseTestClock(useTestClock: Boolean) = copy(useTestClock = useTestClock)
fun setInitialiseSerialization(initialiseSerialization: Boolean) = copy(initialiseSerialization = initialiseSerialization) fun setInitialiseSerialization(initialiseSerialization: Boolean) = copy(initialiseSerialization = initialiseSerialization)
fun setStartNodesInProcess(startNodesInProcess: Boolean) = copy(startNodesInProcess = startNodesInProcess) fun setStartNodesInProcess(startNodesInProcess: Boolean) = copy(startNodesInProcess = startNodesInProcess)
fun setTerminateNodesOnShutdown(terminateNodesOnShutdown: Boolean) = copy(waitForNodesToFinish = terminateNodesOnShutdown) fun setWaitForAllNodesToFinish(waitForAllNodesToFinish: Boolean) = copy(waitForAllNodesToFinish = waitForAllNodesToFinish)
fun setExtraCordappPackagesToScan(extraCordappPackagesToScan: List<String>) = copy(extraCordappPackagesToScan = extraCordappPackagesToScan) fun setExtraCordappPackagesToScan(extraCordappPackagesToScan: List<String>) = copy(extraCordappPackagesToScan = extraCordappPackagesToScan)
fun setNotarySpecs(notarySpecs: List<NotarySpec>) = copy(notarySpecs = notarySpecs) fun setNotarySpecs(notarySpecs: List<NotarySpec>) = copy(notarySpecs = notarySpecs)
} }
/**
* This is a helper method to allow extending of the DSL, along the lines of
* interface SomeOtherExposedDSLInterface : DriverDSLExposedInterface
* interface SomeOtherInternalDSLInterface : DriverDSLInternalInterface, SomeOtherExposedDSLInterface
* class SomeOtherDSL(val driverDSL : DriverDSL) : DriverDSLInternalInterface by driverDSL, SomeOtherInternalDSLInterface
*
* @param coerce We need this explicit coercion witness because we can't put an extra DI : D bound in a `where` clause.
*/
fun <DI : DriverDSLExposedInterface, D : DriverDSLInternalInterface, A> genericDriver(
driverDsl: D,
initialiseSerialization: Boolean = true,
coerce: (D) -> DI,
dsl: DI.() -> A
): A {
val serializationEnv = setGlobalSerialization(initialiseSerialization)
val shutdownHook = addShutdownHook(driverDsl::shutdown)
try {
driverDsl.start()
return dsl(coerce(driverDsl))
} catch (exception: Throwable) {
log.error("Driver shutting down because of exception", exception)
throw exception
} finally {
driverDsl.shutdown()
shutdownHook.cancel()
serializationEnv.unset()
}
}
/**
* This is a helper method to allow extending of the DSL, along the lines of
* interface SomeOtherExposedDSLInterface : DriverDSLExposedInterface
* interface SomeOtherInternalDSLInterface : DriverDSLInternalInterface, SomeOtherExposedDSLInterface
* class SomeOtherDSL(val driverDSL : DriverDSL) : DriverDSLInternalInterface by driverDSL, SomeOtherInternalDSLInterface
*
* @param coerce We need this explicit coercion witness because we can't put an extra DI : D bound in a `where` clause.
*/
fun <DI : DriverDSLExposedInterface, D : DriverDSLInternalInterface, A> genericDriver(
defaultParameters: DriverParameters = DriverParameters(),
isDebug: Boolean = defaultParameters.isDebug,
driverDirectory: Path = defaultParameters.driverDirectory,
portAllocation: PortAllocation = defaultParameters.portAllocation,
debugPortAllocation: PortAllocation = defaultParameters.debugPortAllocation,
systemProperties: Map<String, String> = defaultParameters.systemProperties,
useTestClock: Boolean = defaultParameters.useTestClock,
initialiseSerialization: Boolean = defaultParameters.initialiseSerialization,
waitForNodesToFinish: Boolean = defaultParameters.waitForNodesToFinish,
startNodesInProcess: Boolean = defaultParameters.startNodesInProcess,
notarySpecs: List<NotarySpec>,
extraCordappPackagesToScan: List<String> = defaultParameters.extraCordappPackagesToScan,
driverDslWrapper: (DriverDSL) -> D,
coerce: (D) -> DI, dsl: DI.() -> A
): A {
val serializationEnv = setGlobalSerialization(initialiseSerialization)
val driverDsl = driverDslWrapper(
DriverDSL(
portAllocation = portAllocation,
debugPortAllocation = debugPortAllocation,
systemProperties = systemProperties,
driverDirectory = driverDirectory.toAbsolutePath(),
useTestClock = useTestClock,
isDebug = isDebug,
startNodesInProcess = startNodesInProcess,
waitForNodesToFinish = waitForNodesToFinish,
extraCordappPackagesToScan = extraCordappPackagesToScan,
notarySpecs = notarySpecs
)
)
val shutdownHook = addShutdownHook(driverDsl::shutdown)
try {
driverDsl.start()
return dsl(coerce(driverDsl))
} catch (exception: Throwable) {
log.error("Driver shutting down because of exception", exception)
throw exception
} finally {
driverDsl.shutdown()
shutdownHook.cancel()
serializationEnv.unset()
}
}
fun getTimestampAsDirectoryName(): String {
return DateTimeFormatter.ofPattern("yyyyMMddHHmmss").withZone(UTC).format(Instant.now())
}
class ListenProcessDeathException(hostAndPort: NetworkHostAndPort, listenProcess: Process) :
CordaException("The process that was expected to listen on $hostAndPort has died with status: ${listenProcess.exitValue()}")
/**
* @throws ListenProcessDeathException if [listenProcess] dies before the check succeeds, i.e. the check can't succeed as intended.
*/
fun addressMustBeBound(executorService: ScheduledExecutorService, hostAndPort: NetworkHostAndPort, listenProcess: Process? = null) {
addressMustBeBoundFuture(executorService, hostAndPort, listenProcess).getOrThrow()
}
fun addressMustBeBoundFuture(executorService: ScheduledExecutorService, hostAndPort: NetworkHostAndPort, listenProcess: Process? = null): CordaFuture<Unit> {
return poll(executorService, "address $hostAndPort to bind") {
if (listenProcess != null && !listenProcess.isAlive) {
throw ListenProcessDeathException(hostAndPort, listenProcess)
}
try {
Socket(hostAndPort.host, hostAndPort.port).close()
Unit
} catch (_exception: SocketException) {
null
}
}
}
/*
* The default timeout value of 40 seconds have been chosen based on previous node shutdown time estimate.
* It's been observed that nodes can take up to 30 seconds to shut down, so just to stay on the safe side the 60 seconds
* timeout has been chosen.
*/
fun addressMustNotBeBound(executorService: ScheduledExecutorService, hostAndPort: NetworkHostAndPort, timeout: Duration = 40.seconds) {
addressMustNotBeBoundFuture(executorService, hostAndPort).getOrThrow(timeout)
}
fun addressMustNotBeBoundFuture(executorService: ScheduledExecutorService, hostAndPort: NetworkHostAndPort): CordaFuture<Unit> {
return poll(executorService, "address $hostAndPort to unbind") {
try {
Socket(hostAndPort.host, hostAndPort.port).close()
null
} catch (_exception: SocketException) {
Unit
}
}
}
fun <A> poll(
executorService: ScheduledExecutorService,
pollName: String,
pollInterval: Duration = 500.millis,
warnCount: Int = 120,
check: () -> A?
): CordaFuture<A> {
val resultFuture = openFuture<A>()
val task = object : Runnable {
var counter = -1
override fun run() {
if (resultFuture.isCancelled) return // Give up, caller can no longer get the result.
if (++counter == warnCount) {
log.warn("Been polling $pollName for ${(pollInterval * warnCount.toLong()).seconds} seconds...")
}
try {
val checkResult = check()
if (checkResult != null) {
resultFuture.set(checkResult)
} else {
executorService.schedule(this, pollInterval.toMillis(), MILLISECONDS)
}
} catch (t: Throwable) {
resultFuture.setException(t)
}
}
}
executorService.submit(task) // The check may be expensive, so always run it in the background even the first time.
return resultFuture
}
class DriverDSL(
val portAllocation: PortAllocation,
val debugPortAllocation: PortAllocation,
val systemProperties: Map<String, String>,
val driverDirectory: Path,
val useTestClock: Boolean,
val isDebug: Boolean,
val startNodesInProcess: Boolean,
val waitForNodesToFinish: Boolean,
extraCordappPackagesToScan: List<String>,
val notarySpecs: List<NotarySpec>
) : DriverDSLInternalInterface {
private var _executorService: ScheduledExecutorService? = null
val executorService get() = _executorService!!
private var _shutdownManager: ShutdownManager? = null
override val shutdownManager get() = _shutdownManager!!
private val cordappPackages = extraCordappPackagesToScan + getCallerPackage()
// TODO: this object will copy NodeInfo files from started nodes to other nodes additional-node-infos/
// This uses the FileSystem and adds a delay (~5 seconds) given by the time we wait before polling the file system.
// Investigate whether we can avoid that.
private val nodeInfoFilesCopier = NodeInfoFilesCopier()
// Map from a nodes legal name to an observable emitting the number of nodes in its network map.
private val countObservables = mutableMapOf<CordaX500Name, Observable<Int>>()
private lateinit var _notaries: List<NotaryHandle>
override val notaryHandles: List<NotaryHandle> get() = _notaries
class State {
val processes = ArrayList<Process>()
}
private val state = ThreadBox(State())
//TODO: remove this once we can bundle quasar properly.
private val quasarJarPath: String by lazy {
val cl = ClassLoader.getSystemClassLoader()
val urls = (cl as URLClassLoader).urLs
val quasarPattern = ".*quasar.*\\.jar$".toRegex()
val quasarFileUrl = urls.first { quasarPattern.matches(it.path) }
Paths.get(quasarFileUrl.toURI()).toString()
}
override fun shutdown() {
if (waitForNodesToFinish) {
state.locked {
processes.forEach { it.waitFor() }
}
}
_shutdownManager?.shutdown()
_executorService?.shutdownNow()
}
private fun establishRpc(config: NodeConfiguration, processDeathFuture: CordaFuture<out Process>): CordaFuture<CordaRPCOps> {
val rpcAddress = config.rpcAddress!!
val client = CordaRPCClient(rpcAddress)
val connectionFuture = poll(executorService, "RPC connection") {
try {
client.start(config.rpcUsers[0].username, config.rpcUsers[0].password)
} catch (e: Exception) {
if (processDeathFuture.isDone) throw e
log.error("Exception $e, Retrying RPC connection at $rpcAddress")
null
}
}
return firstOf(connectionFuture, processDeathFuture) {
if (it == processDeathFuture) {
throw ListenProcessDeathException(rpcAddress, processDeathFuture.getOrThrow())
}
val connection = connectionFuture.getOrThrow()
shutdownManager.registerShutdown(connection::close)
connection.proxy
}
}
override fun startNode(
defaultParameters: NodeParameters,
providedName: CordaX500Name?,
rpcUsers: List<User>,
verifierType: VerifierType,
customOverrides: Map<String, Any?>,
startInSameProcess: Boolean?,
maximumHeapSize: String
): CordaFuture<NodeHandle> {
val p2pAddress = portAllocation.nextHostAndPort()
val rpcAddress = portAllocation.nextHostAndPort()
val webAddress = portAllocation.nextHostAndPort()
// TODO: Derive name from the full picked name, don't just wrap the common name
val name = providedName ?: CordaX500Name(organisation = "${oneOf(names).organisation}-${p2pAddress.port}", locality = "London", country = "GB")
val users = rpcUsers.map { it.copy(permissions = it.permissions + DRIVER_REQUIRED_PERMISSIONS) }
val config = ConfigHelper.loadConfig(
baseDirectory = baseDirectory(name),
allowMissingConfig = true,
configOverrides = configOf(
"myLegalName" to name.toString(),
"p2pAddress" to p2pAddress.toString(),
"rpcAddress" to rpcAddress.toString(),
"webAddress" to webAddress.toString(),
"useTestClock" to useTestClock,
"rpcUsers" to if (users.isEmpty()) defaultRpcUserList else users.map { it.toConfig().root().unwrapped() },
"verifierType" to verifierType.name
) + customOverrides
)
return startNodeInternal(config, webAddress, startInSameProcess, maximumHeapSize)
}
internal fun startCordformNode(cordform: CordformNode): CordaFuture<NodeHandle> {
val name = CordaX500Name.parse(cordform.name)
// TODO We shouldn't have to allocate an RPC or web address if they're not specified. We're having to do this because of startNodeInternal
val rpcAddress = if (cordform.rpcAddress == null) mapOf("rpcAddress" to portAllocation.nextHostAndPort().toString()) else emptyMap()
val webAddress = cordform.webAddress?.let { NetworkHostAndPort.parse(it) } ?: portAllocation.nextHostAndPort()
val notary = if (cordform.notary != null) mapOf("notary" to cordform.notary) else emptyMap()
val rpcUsers = cordform.rpcUsers
val config = ConfigHelper.loadConfig(
baseDirectory = baseDirectory(name),
allowMissingConfig = true,
configOverrides = cordform.config + rpcAddress + notary + mapOf(
"rpcUsers" to if (rpcUsers.isEmpty()) defaultRpcUserList else rpcUsers
)
)
return startNodeInternal(config, webAddress, null, "200m")
}
private fun queryWebserver(handle: NodeHandle, process: Process): WebserverHandle {
val protocol = if (handle.configuration.useHTTPS) "https://" else "http://"
val url = URL("$protocol${handle.webAddress}/api/status")
val client = OkHttpClient.Builder().connectTimeout(5, SECONDS).readTimeout(60, SECONDS).build()
while (process.isAlive) try {
val response = client.newCall(Request.Builder().url(url).build()).execute()
if (response.isSuccessful && (response.body().string() == "started")) {
return WebserverHandle(handle.webAddress, process)
}
} catch (e: ConnectException) {
log.debug("Retrying webserver info at ${handle.webAddress}")
}
throw IllegalStateException("Webserver at ${handle.webAddress} has died")
}
override fun startWebserver(handle: NodeHandle, maximumHeapSize: String): CordaFuture<WebserverHandle> {
val debugPort = if (isDebug) debugPortAllocation.nextPort() else null
val process = DriverDSL.startWebserver(handle, debugPort, maximumHeapSize)
shutdownManager.registerProcessShutdown(process)
val webReadyFuture = addressMustBeBoundFuture(executorService, handle.webAddress, process)
return webReadyFuture.map { queryWebserver(handle, process) }
}
override fun start() {
_executorService = Executors.newScheduledThreadPool(2, ThreadFactoryBuilder().setNameFormat("driver-pool-thread-%d").build())
_shutdownManager = ShutdownManager(executorService)
shutdownManager.registerShutdown { nodeInfoFilesCopier.close() }
val notaryInfos = generateNotaryIdentities()
val nodeHandles = startNotaries()
_notaries = notaryInfos.zip(nodeHandles) { (identity, validating), nodes -> NotaryHandle(identity, validating, nodes) }
}
private fun generateNotaryIdentities(): List<Pair<Party, Boolean>> {
return notarySpecs.map { spec ->
val identity = if (spec.cluster == null) {
ServiceIdentityGenerator.generateToDisk(
dirs = listOf(baseDirectory(spec.name)),
serviceName = spec.name.copy(commonName = NotaryService.constructId(validating = spec.validating))
)
} else {
ServiceIdentityGenerator.generateToDisk(
dirs = generateNodeNames(spec).map { baseDirectory(it) },
serviceName = spec.name
)
}
Pair(identity, spec.validating)
}
}
private fun generateNodeNames(spec: NotarySpec): List<CordaX500Name> {
return (0 until spec.cluster!!.clusterSize).map { spec.name.copy(commonName = null, organisation = "${spec.name.organisation}-$it") }
}
private fun startNotaries(): List<CordaFuture<List<NodeHandle>>> {
return notarySpecs.map {
when {
it.cluster == null -> startSingleNotary(it)
it.cluster is ClusterSpec.Raft -> startRaftNotaryCluster(it)
else -> throw IllegalArgumentException("BFT-SMaRt not supported")
}
}
}
// TODO This mapping is done is several places including the gradle plugin. In general we need a better way of
// generating the configs for the nodes, probably making use of Any.toConfig()
private fun NotaryConfig.toConfigMap(): Map<String, Any> = mapOf("notary" to toConfig().root().unwrapped())
private fun startSingleNotary(spec: NotarySpec): CordaFuture<List<NodeHandle>> {
return startNode(
providedName = spec.name,
rpcUsers = spec.rpcUsers,
verifierType = spec.verifierType,
customOverrides = NotaryConfig(spec.validating).toConfigMap()
).map { listOf(it) }
}
private fun startRaftNotaryCluster(spec: NotarySpec): CordaFuture<List<NodeHandle>> {
fun notaryConfig(nodeAddress: NetworkHostAndPort, clusterAddress: NetworkHostAndPort? = null): Map<String, Any> {
val clusterAddresses = if (clusterAddress != null) listOf(clusterAddress) else emptyList()
val config = NotaryConfig(
validating = spec.validating,
raft = RaftConfig(nodeAddress = nodeAddress, clusterAddresses = clusterAddresses))
return config.toConfigMap()
}
val nodeNames = generateNodeNames(spec)
val clusterAddress = portAllocation.nextHostAndPort()
// Start the first node that will bootstrap the cluster
val firstNodeFuture = startNode(
providedName = nodeNames[0],
rpcUsers = spec.rpcUsers,
verifierType = spec.verifierType,
customOverrides = notaryConfig(clusterAddress) + mapOf(
"database.serverNameTablePrefix" to nodeNames[0].toString().replace(Regex("[^0-9A-Za-z]+"), "")
)
)
// All other nodes will join the cluster
val restNodeFutures = nodeNames.drop(1).map {
val nodeAddress = portAllocation.nextHostAndPort()
startNode(
providedName = it,
rpcUsers = spec.rpcUsers,
verifierType = spec.verifierType,
customOverrides = notaryConfig(nodeAddress, clusterAddress) + mapOf(
"database.serverNameTablePrefix" to it.toString().replace(Regex("[^0-9A-Za-z]+"), "")
)
)
}
return firstNodeFuture.flatMap { first ->
restNodeFutures.transpose().map { rest -> listOf(first) + rest }
}
}
fun baseDirectory(nodeName: CordaX500Name): Path {
val nodeDirectoryName = nodeName.organisation.filter { !it.isWhitespace() }
return driverDirectory / nodeDirectoryName
}
override fun baseDirectory(nodeName: String): Path = baseDirectory(CordaX500Name.parse(nodeName))
/**
* @param initial number of nodes currently in the network map of a running node.
* @param networkMapCacheChangeObservable an observable returning the updates to the node network map.
* @return a [ConnectableObservable] which emits a new [Int] every time the number of registered nodes changes
* the initial value emitted is always [initial]
*/
private fun nodeCountObservable(initial: Int, networkMapCacheChangeObservable: Observable<NetworkMapCache.MapChange>):
ConnectableObservable<Int> {
val count = AtomicInteger(initial)
return networkMapCacheChangeObservable.map { it ->
when (it) {
is NetworkMapCache.MapChange.Added -> count.incrementAndGet()
is NetworkMapCache.MapChange.Removed -> count.decrementAndGet()
is NetworkMapCache.MapChange.Modified -> count.get()
}
}.startWith(initial).replay()
}
/**
* @param rpc the [CordaRPCOps] of a newly started node.
* @return a [CordaFuture] which resolves when every node started by driver has in its network map a number of nodes
* equal to the number of running nodes. The future will yield the number of connected nodes.
*/
private fun allNodesConnected(rpc: CordaRPCOps): CordaFuture<Int> {
val (snapshot, updates) = rpc.networkMapFeed()
val counterObservable = nodeCountObservable(snapshot.size, updates)
countObservables[rpc.nodeInfo().legalIdentities[0].name] = counterObservable
/* TODO: this might not always be the exact number of nodes one has to wait for,
* for example in the following sequence
* 1 start 3 nodes in order, A, B, C.
* 2 before the future returned by this function resolves, kill B
* At that point this future won't ever resolve as it will wait for nodes to know 3 other nodes.
*/
val requiredNodes = countObservables.size
// This is an observable which yield the minimum number of nodes in each node network map.
val smallestSeenNetworkMapSize = Observable.combineLatest(countObservables.values.toList()) { args: Array<Any> ->
args.map { it as Int }.min() ?: 0
}
val future = smallestSeenNetworkMapSize.filter { it >= requiredNodes }.toFuture()
counterObservable.connect()
return future
}
private fun startNodeInternal(config: Config,
webAddress: NetworkHostAndPort,
startInProcess: Boolean?,
maximumHeapSize: String): CordaFuture<NodeHandle> {
val configuration = config.parseAsNodeConfiguration()
val baseDirectory = configuration.baseDirectory.createDirectories()
nodeInfoFilesCopier.addConfig(baseDirectory)
val onNodeExit: () -> Unit = {
nodeInfoFilesCopier.removeConfig(baseDirectory)
countObservables.remove(configuration.myLegalName)
}
if (startInProcess ?: startNodesInProcess) {
val nodeAndThreadFuture = startInProcessNode(executorService, configuration, config, cordappPackages)
shutdownManager.registerShutdown(
nodeAndThreadFuture.map { (node, thread) ->
{
node.dispose()
thread.interrupt()
}
}
)
return nodeAndThreadFuture.flatMap { (node, thread) ->
establishRpc(configuration, openFuture()).flatMap { rpc ->
allNodesConnected(rpc).map {
NodeHandle.InProcess(rpc.nodeInfo(), rpc, configuration, webAddress, node, thread, onNodeExit)
}
}
}
} else {
val debugPort = if (isDebug) debugPortAllocation.nextPort() else null
val process = startOutOfProcessNode(configuration, config, quasarJarPath, debugPort, systemProperties, cordappPackages, maximumHeapSize)
if (waitForNodesToFinish) {
state.locked {
processes += process
}
} else {
shutdownManager.registerProcessShutdown(process)
}
val p2pReadyFuture = addressMustBeBoundFuture(executorService, configuration.p2pAddress, process)
return p2pReadyFuture.flatMap {
val processDeathFuture = poll(executorService, "process death") {
if (process.isAlive) null else process
}
establishRpc(configuration, processDeathFuture).flatMap { rpc ->
// Check for all nodes to have all other nodes in background in case RPC is failing over:
val networkMapFuture = executorService.fork { allNodesConnected(rpc) }.flatMap { it }
firstOf(processDeathFuture, networkMapFuture) {
if (it == processDeathFuture) {
throw ListenProcessDeathException(configuration.p2pAddress, process)
}
processDeathFuture.cancel(false)
log.info("Node handle is ready. NodeInfo: ${rpc.nodeInfo()}, WebAddress: $webAddress")
NodeHandle.OutOfProcess(rpc.nodeInfo(), rpc, configuration, webAddress, debugPort, process,
onNodeExit)
}
}
}
}
}
override fun <A> pollUntilNonNull(pollName: String, pollInterval: Duration, warnCount: Int, check: () -> A?): CordaFuture<A> {
val pollFuture = poll(executorService, pollName, pollInterval, warnCount, check)
shutdownManager.registerShutdown { pollFuture.cancel(true) }
return pollFuture
}
companion object {
private val defaultRpcUserList = listOf(User("default", "default", setOf("ALL")).toConfig().root().unwrapped())
private val names = arrayOf(
ALICE.name,
BOB.name,
DUMMY_BANK_A.name
)
private fun <A> oneOf(array: Array<A>) = array[Random().nextInt(array.size)]
private fun startInProcessNode(
executorService: ScheduledExecutorService,
nodeConf: NodeConfiguration,
config: Config,
cordappPackages: List<String>
): CordaFuture<Pair<StartedNode<Node>, Thread>> {
return executorService.fork {
log.info("Starting in-process Node ${nodeConf.myLegalName.organisation}")
// Write node.conf
writeConfig(nodeConf.baseDirectory, "node.conf", config)
// TODO pass the version in?
val node = InProcessNode(nodeConf, MOCK_VERSION_INFO, cordappPackages).start()
val nodeThread = thread(name = nodeConf.myLegalName.organisation) {
node.internals.run()
}
node to nodeThread
}.flatMap {
nodeAndThread -> addressMustBeBoundFuture(executorService, nodeConf.p2pAddress).map { nodeAndThread }
}
}
private fun startOutOfProcessNode(
nodeConf: NodeConfiguration,
config: Config,
quasarJarPath: String,
debugPort: Int?,
overriddenSystemProperties: Map<String, String>,
cordappPackages: List<String>,
maximumHeapSize: String
): Process {
log.info("Starting out-of-process Node ${nodeConf.myLegalName.organisation}, debug port is " + (debugPort ?: "not enabled"))
// Write node.conf
writeConfig(nodeConf.baseDirectory, "node.conf", config)
val systemProperties = overriddenSystemProperties + mapOf(
"name" to nodeConf.myLegalName,
"visualvm.display.name" to "corda-${nodeConf.myLegalName}",
Node.scanPackagesSystemProperty to cordappPackages.joinToString(Node.scanPackagesSeparator),
"java.io.tmpdir" to System.getProperty("java.io.tmpdir"), // Inherit from parent process
"log4j2.debug" to if(debugPort != null) "true" else "false"
)
// See experimental/quasar-hook/README.md for how to generate.
val excludePattern = "x(antlr**;bftsmart**;ch**;co.paralleluniverse**;com.codahale**;com.esotericsoftware**;" +
"com.fasterxml**;com.google**;com.ibm**;com.intellij**;com.jcabi**;com.nhaarman**;com.opengamma**;" +
"com.typesafe**;com.zaxxer**;de.javakaffee**;groovy**;groovyjarjarantlr**;groovyjarjarasm**;io.atomix**;" +
"io.github**;io.netty**;jdk**;joptsimple**;junit**;kotlin**;net.bytebuddy**;net.i2p**;org.apache**;" +
"org.assertj**;org.bouncycastle**;org.codehaus**;org.crsh**;org.dom4j**;org.fusesource**;org.h2**;" +
"org.hamcrest**;org.hibernate**;org.jboss**;org.jcp**;org.joda**;org.junit**;org.mockito**;org.objectweb**;" +
"org.objenesis**;org.slf4j**;org.w3c**;org.xml**;org.yaml**;reflectasm**;rx**)"
val extraJvmArguments = systemProperties.removeResolvedClasspath().map { "-D${it.key}=${it.value}" } +
"-javaagent:$quasarJarPath=$excludePattern"
val loggingLevel = if (debugPort == null) "INFO" else "DEBUG"
return ProcessUtilities.startCordaProcess(
className = "net.corda.node.Corda", // cannot directly get class for this, so just use string
arguments = listOf(
"--base-directory=${nodeConf.baseDirectory}",
"--logging-level=$loggingLevel",
"--no-local-shell"
),
jdwpPort = debugPort,
extraJvmArguments = extraJvmArguments,
errorLogPath = nodeConf.baseDirectory / NodeStartup.LOGS_DIRECTORY_NAME / "error.log",
workingDirectory = nodeConf.baseDirectory,
maximumHeapSize = maximumHeapSize
)
}
private fun startWebserver(handle: NodeHandle, debugPort: Int?, maximumHeapSize: String): Process {
val className = "net.corda.webserver.WebServer"
return ProcessUtilities.startCordaProcess(
className = className, // cannot directly get class for this, so just use string
arguments = listOf("--base-directory", handle.configuration.baseDirectory.toString()),
jdwpPort = debugPort,
extraJvmArguments = listOf(
"-Dname=node-${handle.configuration.p2pAddress}-webserver",
"-Djava.io.tmpdir=${System.getProperty("java.io.tmpdir")}" // Inherit from parent process
),
errorLogPath = Paths.get("error.$className.log"),
workingDirectory = null,
maximumHeapSize = maximumHeapSize
)
}
private fun getCallerPackage(): String {
return Exception()
.stackTrace
.first { it.fileName != "Driver.kt" }
.let { Class.forName(it.className).`package`?.name }
?: throw IllegalStateException("Function instantiating driver must be defined in a package.")
}
/**
* We have an alternative way of specifying classpath for spawned process: by using "-cp" option. So duplicating the setting of this
* rather long string is un-necessary and can be harmful on Windows.
*/
private fun Map<String, Any>.removeResolvedClasspath(): Map<String, Any> {
return filterNot { it.key == "java.class.path" }
}
}
}
fun writeConfig(path: Path, filename: String, config: Config) {
val configString = config.root().render(ConfigRenderOptions.defaults())
configString.byteInputStream().copyTo(path / filename, REPLACE_EXISTING)
}

View File

@ -0,0 +1,92 @@
package net.corda.testing.driver
import net.corda.core.concurrent.CordaFuture
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.internal.concurrent.map
import net.corda.node.internal.Node
import net.corda.node.services.config.VerifierType
import net.corda.nodeapi.internal.config.User
import net.corda.testing.node.NotarySpec
import java.nio.file.Path
interface DriverDSL {
/** Returns a list of [NotaryHandle]s matching the list of [NotarySpec]s passed into [driver]. */
val notaryHandles: List<NotaryHandle>
/**
* Returns the [NotaryHandle] for the single notary on the network. Throws if there are none or more than one.
* @see notaryHandles
*/
val defaultNotaryHandle: NotaryHandle get() {
return when (notaryHandles.size) {
0 -> throw IllegalStateException("There are no notaries defined on the network")
1 -> notaryHandles[0]
else -> throw IllegalStateException("There is more than one notary defined on the network")
}
}
/**
* Returns the identity of the single notary on the network. Throws if there are none or more than one.
* @see defaultNotaryHandle
*/
val defaultNotaryIdentity: Party get() = defaultNotaryHandle.identity
/**
* Returns a [CordaFuture] on the [NodeHandle] for the single-node notary on the network. Throws if there
* are no notaries or more than one, or if the notary is a distributed cluster.
* @see defaultNotaryHandle
* @see notaryHandles
*/
val defaultNotaryNode: CordaFuture<NodeHandle> get() {
return defaultNotaryHandle.nodeHandles.map {
it.singleOrNull() ?: throw IllegalStateException("Default notary is not a single node")
}
}
/**
* Start a node.
*
* @param defaultParameters The default parameters for the node. Allows the node to be configured in builder style
* when called from Java code.
* @param providedName Optional name of the node, which will be its legal name in [Party]. Defaults to something
* random. Note that this must be unique as the driver uses it as a primary key!
* @param verifierType The type of transaction verifier to use. See: [VerifierType]
* @param rpcUsers List of users who are authorised to use the RPC system. Defaults to empty list.
* @param startInSameProcess Determines if the node should be started inside the same process the Driver is running
* in. If null the Driver-level value will be used.
* @return A [CordaFuture] on the [NodeHandle] to the node. The future will complete when the node is available.
*/
fun startNode(
defaultParameters: NodeParameters = NodeParameters(),
providedName: CordaX500Name? = defaultParameters.providedName,
rpcUsers: List<User> = defaultParameters.rpcUsers,
verifierType: VerifierType = defaultParameters.verifierType,
customOverrides: Map<String, Any?> = defaultParameters.customOverrides,
startInSameProcess: Boolean? = defaultParameters.startInSameProcess,
maximumHeapSize: String = defaultParameters.maximumHeapSize): CordaFuture<NodeHandle>
/**
* Helper function for starting a [Node] with custom parameters from Java.
*
* @param parameters The default parameters for the driver.
* @return [NodeHandle] that will be available sometime in the future.
*/
fun startNode(parameters: NodeParameters): CordaFuture<NodeHandle> = startNode(defaultParameters = parameters)
/** Call [startWebserver] with a default maximumHeapSize. */
fun startWebserver(handle: NodeHandle): CordaFuture<WebserverHandle> = startWebserver(handle, "200m")
/**
* Starts a web server for a node
* @param handle The handle for the node that this webserver connects to via RPC.
* @param maximumHeapSize Argument for JVM -Xmx option e.g. "200m".
*/
fun startWebserver(handle: NodeHandle, maximumHeapSize: String): CordaFuture<WebserverHandle>
/**
* Returns the base directory for a node with the given [CordaX500Name]. This method is useful if the base directory
* is needed before the node is started.
*/
fun baseDirectory(nodeName: CordaX500Name): Path
}

View File

@ -0,0 +1,683 @@
package net.corda.testing.internal
import com.google.common.util.concurrent.ThreadFactoryBuilder
import com.typesafe.config.Config
import com.typesafe.config.ConfigRenderOptions
import net.corda.client.rpc.CordaRPCClient
import net.corda.cordform.CordformContext
import net.corda.cordform.CordformNode
import net.corda.core.concurrent.CordaFuture
import net.corda.core.concurrent.firstOf
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.internal.ThreadBox
import net.corda.core.internal.concurrent.*
import net.corda.core.internal.copyTo
import net.corda.core.internal.createDirectories
import net.corda.core.internal.div
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.node.services.NetworkMapCache
import net.corda.core.node.services.NotaryService
import net.corda.core.toFuture
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.node.internal.Node
import net.corda.node.internal.NodeStartup
import net.corda.node.internal.StartedNode
import net.corda.node.services.Permissions
import net.corda.node.services.config.*
import net.corda.node.utilities.ServiceIdentityGenerator
import net.corda.nodeapi.NodeInfoFilesCopier
import net.corda.nodeapi.internal.addShutdownHook
import net.corda.nodeapi.internal.config.User
import net.corda.nodeapi.internal.config.toConfig
import net.corda.testing.ALICE
import net.corda.testing.BOB
import net.corda.testing.DUMMY_BANK_A
import net.corda.testing.driver.*
import net.corda.testing.node.ClusterSpec
import net.corda.testing.node.MockServices.Companion.MOCK_VERSION_INFO
import net.corda.testing.node.NotarySpec
import net.corda.testing.setGlobalSerialization
import okhttp3.OkHttpClient
import okhttp3.Request
import rx.Observable
import rx.observables.ConnectableObservable
import java.net.ConnectException
import java.net.URL
import java.net.URLClassLoader
import java.nio.file.Path
import java.nio.file.Paths
import java.nio.file.StandardCopyOption
import java.time.Duration
import java.time.Instant
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import java.util.*
import java.util.concurrent.Executors
import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger
import kotlin.concurrent.thread
class DriverDSLImpl(
val portAllocation: PortAllocation,
val debugPortAllocation: PortAllocation,
val systemProperties: Map<String, String>,
val driverDirectory: Path,
val useTestClock: Boolean,
val isDebug: Boolean,
val startNodesInProcess: Boolean,
val waitForNodesToFinish: Boolean,
extraCordappPackagesToScan: List<String>,
val notarySpecs: List<NotarySpec>
) : InternalDriverDSL {
private var _executorService: ScheduledExecutorService? = null
val executorService get() = _executorService!!
private var _shutdownManager: ShutdownManager? = null
override val shutdownManager get() = _shutdownManager!!
private val cordappPackages = extraCordappPackagesToScan + getCallerPackage()
// TODO: this object will copy NodeInfo files from started nodes to other nodes additional-node-infos/
// This uses the FileSystem and adds a delay (~5 seconds) given by the time we wait before polling the file system.
// Investigate whether we can avoid that.
private val nodeInfoFilesCopier = NodeInfoFilesCopier()
// Map from a nodes legal name to an observable emitting the number of nodes in its network map.
private val countObservables = mutableMapOf<CordaX500Name, Observable<Int>>()
private lateinit var _notaries: List<NotaryHandle>
override val notaryHandles: List<NotaryHandle> get() = _notaries
class State {
val processes = ArrayList<Process>()
}
private val state = ThreadBox(State())
//TODO: remove this once we can bundle quasar properly.
private val quasarJarPath: String by lazy {
val cl = ClassLoader.getSystemClassLoader()
val urls = (cl as URLClassLoader).urLs
val quasarPattern = ".*quasar.*\\.jar$".toRegex()
val quasarFileUrl = urls.first { quasarPattern.matches(it.path) }
Paths.get(quasarFileUrl.toURI()).toString()
}
override fun shutdown() {
if (waitForNodesToFinish) {
state.locked {
processes.forEach { it.waitFor() }
}
}
_shutdownManager?.shutdown()
_executorService?.shutdownNow()
}
private fun establishRpc(config: NodeConfiguration, processDeathFuture: CordaFuture<out Process>): CordaFuture<CordaRPCOps> {
val rpcAddress = config.rpcAddress!!
val client = CordaRPCClient(rpcAddress)
val connectionFuture = poll(executorService, "RPC connection") {
try {
client.start(config.rpcUsers[0].username, config.rpcUsers[0].password)
} catch (e: Exception) {
if (processDeathFuture.isDone) throw e
log.error("Exception $e, Retrying RPC connection at $rpcAddress")
null
}
}
return firstOf(connectionFuture, processDeathFuture) {
if (it == processDeathFuture) {
throw ListenProcessDeathException(rpcAddress, processDeathFuture.getOrThrow())
}
val connection = connectionFuture.getOrThrow()
shutdownManager.registerShutdown(connection::close)
connection.proxy
}
}
override fun startNode(
defaultParameters: NodeParameters,
providedName: CordaX500Name?,
rpcUsers: List<User>,
verifierType: VerifierType,
customOverrides: Map<String, Any?>,
startInSameProcess: Boolean?,
maximumHeapSize: String
): CordaFuture<NodeHandle> {
val p2pAddress = portAllocation.nextHostAndPort()
val rpcAddress = portAllocation.nextHostAndPort()
val webAddress = portAllocation.nextHostAndPort()
// TODO: Derive name from the full picked name, don't just wrap the common name
val name = providedName ?: CordaX500Name(organisation = "${oneOf(names).organisation}-${p2pAddress.port}", locality = "London", country = "GB")
val users = rpcUsers.map { it.copy(permissions = it.permissions + DRIVER_REQUIRED_PERMISSIONS) }
val config = ConfigHelper.loadConfig(
baseDirectory = baseDirectory(name),
allowMissingConfig = true,
configOverrides = configOf(
"myLegalName" to name.toString(),
"p2pAddress" to p2pAddress.toString(),
"rpcAddress" to rpcAddress.toString(),
"webAddress" to webAddress.toString(),
"useTestClock" to useTestClock,
"rpcUsers" to if (users.isEmpty()) defaultRpcUserList else users.map { it.toConfig().root().unwrapped() },
"verifierType" to verifierType.name
) + customOverrides
)
return startNodeInternal(config, webAddress, startInSameProcess, maximumHeapSize)
}
internal fun startCordformNode(cordform: CordformNode): CordaFuture<NodeHandle> {
val name = CordaX500Name.parse(cordform.name)
// TODO We shouldn't have to allocate an RPC or web address if they're not specified. We're having to do this because of startNodeInternal
val rpcAddress = if (cordform.rpcAddress == null) mapOf("rpcAddress" to portAllocation.nextHostAndPort().toString()) else emptyMap()
val webAddress = cordform.webAddress?.let { NetworkHostAndPort.parse(it) } ?: portAllocation.nextHostAndPort()
val notary = if (cordform.notary != null) mapOf("notary" to cordform.notary) else emptyMap()
val rpcUsers = cordform.rpcUsers
val config = ConfigHelper.loadConfig(
baseDirectory = baseDirectory(name),
allowMissingConfig = true,
configOverrides = cordform.config + rpcAddress + notary + mapOf(
"rpcUsers" to if (rpcUsers.isEmpty()) defaultRpcUserList else rpcUsers
)
)
return startNodeInternal(config, webAddress, null, "200m")
}
private fun queryWebserver(handle: NodeHandle, process: Process): WebserverHandle {
val protocol = if (handle.configuration.useHTTPS) "https://" else "http://"
val url = URL("$protocol${handle.webAddress}/api/status")
val client = OkHttpClient.Builder().connectTimeout(5, TimeUnit.SECONDS).readTimeout(60, TimeUnit.SECONDS).build()
while (process.isAlive) try {
val response = client.newCall(Request.Builder().url(url).build()).execute()
if (response.isSuccessful && (response.body().string() == "started")) {
return WebserverHandle(handle.webAddress, process)
}
} catch (e: ConnectException) {
log.debug("Retrying webserver info at ${handle.webAddress}")
}
throw IllegalStateException("Webserver at ${handle.webAddress} has died")
}
override fun startWebserver(handle: NodeHandle, maximumHeapSize: String): CordaFuture<WebserverHandle> {
val debugPort = if (isDebug) debugPortAllocation.nextPort() else null
val process = startWebserver(handle, debugPort, maximumHeapSize)
shutdownManager.registerProcessShutdown(process)
val webReadyFuture = addressMustBeBoundFuture(executorService, handle.webAddress, process)
return webReadyFuture.map { queryWebserver(handle, process) }
}
override fun start() {
_executorService = Executors.newScheduledThreadPool(2, ThreadFactoryBuilder().setNameFormat("driver-pool-thread-%d").build())
_shutdownManager = ShutdownManager(executorService)
shutdownManager.registerShutdown { nodeInfoFilesCopier.close() }
val notaryInfos = generateNotaryIdentities()
val nodeHandles = startNotaries()
_notaries = notaryInfos.zip(nodeHandles) { (identity, validating), nodes -> NotaryHandle(identity, validating, nodes) }
}
private fun generateNotaryIdentities(): List<Pair<Party, Boolean>> {
return notarySpecs.map { spec ->
val identity = if (spec.cluster == null) {
ServiceIdentityGenerator.generateToDisk(
dirs = listOf(baseDirectory(spec.name)),
serviceName = spec.name.copy(commonName = NotaryService.constructId(validating = spec.validating))
)
} else {
ServiceIdentityGenerator.generateToDisk(
dirs = generateNodeNames(spec).map { baseDirectory(it) },
serviceName = spec.name
)
}
Pair(identity, spec.validating)
}
}
private fun generateNodeNames(spec: NotarySpec): List<CordaX500Name> {
return (0 until spec.cluster!!.clusterSize).map { spec.name.copy(commonName = null, organisation = "${spec.name.organisation}-$it") }
}
private fun startNotaries(): List<CordaFuture<List<NodeHandle>>> {
return notarySpecs.map {
when {
it.cluster == null -> startSingleNotary(it)
it.cluster is ClusterSpec.Raft -> startRaftNotaryCluster(it)
else -> throw IllegalArgumentException("BFT-SMaRt not supported")
}
}
}
// TODO This mapping is done is several places including the gradle plugin. In general we need a better way of
// generating the configs for the nodes, probably making use of Any.toConfig()
private fun NotaryConfig.toConfigMap(): Map<String, Any> = mapOf("notary" to toConfig().root().unwrapped())
private fun startSingleNotary(spec: NotarySpec): CordaFuture<List<NodeHandle>> {
return startNode(
providedName = spec.name,
rpcUsers = spec.rpcUsers,
verifierType = spec.verifierType,
customOverrides = NotaryConfig(spec.validating).toConfigMap()
).map { listOf(it) }
}
private fun startRaftNotaryCluster(spec: NotarySpec): CordaFuture<List<NodeHandle>> {
fun notaryConfig(nodeAddress: NetworkHostAndPort, clusterAddress: NetworkHostAndPort? = null): Map<String, Any> {
val clusterAddresses = if (clusterAddress != null) listOf(clusterAddress) else emptyList()
val config = NotaryConfig(
validating = spec.validating,
raft = RaftConfig(nodeAddress = nodeAddress, clusterAddresses = clusterAddresses))
return config.toConfigMap()
}
val nodeNames = generateNodeNames(spec)
val clusterAddress = portAllocation.nextHostAndPort()
// Start the first node that will bootstrap the cluster
val firstNodeFuture = startNode(
providedName = nodeNames[0],
rpcUsers = spec.rpcUsers,
verifierType = spec.verifierType,
customOverrides = notaryConfig(clusterAddress) + mapOf(
"database.serverNameTablePrefix" to nodeNames[0].toString().replace(Regex("[^0-9A-Za-z]+"), "")
)
)
// All other nodes will join the cluster
val restNodeFutures = nodeNames.drop(1).map {
val nodeAddress = portAllocation.nextHostAndPort()
startNode(
providedName = it,
rpcUsers = spec.rpcUsers,
verifierType = spec.verifierType,
customOverrides = notaryConfig(nodeAddress, clusterAddress) + mapOf(
"database.serverNameTablePrefix" to it.toString().replace(Regex("[^0-9A-Za-z]+"), "")
)
)
}
return firstNodeFuture.flatMap { first ->
restNodeFutures.transpose().map { rest -> listOf(first) + rest }
}
}
override fun baseDirectory(nodeName: CordaX500Name): Path {
val nodeDirectoryName = nodeName.organisation.filter { !it.isWhitespace() }
return driverDirectory / nodeDirectoryName
}
/**
* @param initial number of nodes currently in the network map of a running node.
* @param networkMapCacheChangeObservable an observable returning the updates to the node network map.
* @return a [ConnectableObservable] which emits a new [Int] every time the number of registered nodes changes
* the initial value emitted is always [initial]
*/
private fun nodeCountObservable(initial: Int, networkMapCacheChangeObservable: Observable<NetworkMapCache.MapChange>):
ConnectableObservable<Int> {
val count = AtomicInteger(initial)
return networkMapCacheChangeObservable.map { it ->
when (it) {
is NetworkMapCache.MapChange.Added -> count.incrementAndGet()
is NetworkMapCache.MapChange.Removed -> count.decrementAndGet()
is NetworkMapCache.MapChange.Modified -> count.get()
}
}.startWith(initial).replay()
}
/**
* @param rpc the [CordaRPCOps] of a newly started node.
* @return a [CordaFuture] which resolves when every node started by driver has in its network map a number of nodes
* equal to the number of running nodes. The future will yield the number of connected nodes.
*/
private fun allNodesConnected(rpc: CordaRPCOps): CordaFuture<Int> {
val (snapshot, updates) = rpc.networkMapFeed()
val counterObservable = nodeCountObservable(snapshot.size, updates)
countObservables[rpc.nodeInfo().legalIdentities[0].name] = counterObservable
/* TODO: this might not always be the exact number of nodes one has to wait for,
* for example in the following sequence
* 1 start 3 nodes in order, A, B, C.
* 2 before the future returned by this function resolves, kill B
* At that point this future won't ever resolve as it will wait for nodes to know 3 other nodes.
*/
val requiredNodes = countObservables.size
// This is an observable which yield the minimum number of nodes in each node network map.
val smallestSeenNetworkMapSize = Observable.combineLatest(countObservables.values.toList()) { args: Array<Any> ->
args.map { it as Int }.min() ?: 0
}
val future = smallestSeenNetworkMapSize.filter { it >= requiredNodes }.toFuture()
counterObservable.connect()
return future
}
private fun startNodeInternal(config: Config,
webAddress: NetworkHostAndPort,
startInProcess: Boolean?,
maximumHeapSize: String): CordaFuture<NodeHandle> {
val configuration = config.parseAsNodeConfiguration()
val baseDirectory = configuration.baseDirectory.createDirectories()
nodeInfoFilesCopier.addConfig(baseDirectory)
val onNodeExit: () -> Unit = {
nodeInfoFilesCopier.removeConfig(baseDirectory)
countObservables.remove(configuration.myLegalName)
}
if (startInProcess ?: startNodesInProcess) {
val nodeAndThreadFuture = startInProcessNode(executorService, configuration, config, cordappPackages)
shutdownManager.registerShutdown(
nodeAndThreadFuture.map { (node, thread) ->
{
node.dispose()
thread.interrupt()
}
}
)
return nodeAndThreadFuture.flatMap { (node, thread) ->
establishRpc(configuration, openFuture()).flatMap { rpc ->
allNodesConnected(rpc).map {
NodeHandle.InProcess(rpc.nodeInfo(), rpc, configuration, webAddress, node, thread, onNodeExit)
}
}
}
} else {
val debugPort = if (isDebug) debugPortAllocation.nextPort() else null
val process = startOutOfProcessNode(configuration, config, quasarJarPath, debugPort, systemProperties, cordappPackages, maximumHeapSize)
if (waitForNodesToFinish) {
state.locked {
processes += process
}
} else {
shutdownManager.registerProcessShutdown(process)
}
val p2pReadyFuture = addressMustBeBoundFuture(executorService, configuration.p2pAddress, process)
return p2pReadyFuture.flatMap {
val processDeathFuture = poll(executorService, "process death") {
if (process.isAlive) null else process
}
establishRpc(configuration, processDeathFuture).flatMap { rpc ->
// Check for all nodes to have all other nodes in background in case RPC is failing over:
val networkMapFuture = executorService.fork { allNodesConnected(rpc) }.flatMap { it }
firstOf(processDeathFuture, networkMapFuture) {
if (it == processDeathFuture) {
throw ListenProcessDeathException(configuration.p2pAddress, process)
}
processDeathFuture.cancel(false)
log.info("Node handle is ready. NodeInfo: ${rpc.nodeInfo()}, WebAddress: $webAddress")
NodeHandle.OutOfProcess(rpc.nodeInfo(), rpc, configuration, webAddress, debugPort, process,
onNodeExit)
}
}
}
}
}
override fun <A> pollUntilNonNull(pollName: String, pollInterval: Duration, warnCount: Int, check: () -> A?): CordaFuture<A> {
val pollFuture = poll(executorService, pollName, pollInterval, warnCount, check)
shutdownManager.registerShutdown { pollFuture.cancel(true) }
return pollFuture
}
companion object {
internal val log = contextLogger()
private val defaultRpcUserList = listOf(User("default", "default", setOf("ALL")).toConfig().root().unwrapped())
private val names = arrayOf(
ALICE.name,
BOB.name,
DUMMY_BANK_A.name
)
/**
* A sub-set of permissions that grant most of the essential operations used in the unit/integration tests as well as
* in demo application like NodeExplorer.
*/
private val DRIVER_REQUIRED_PERMISSIONS = setOf(
Permissions.invokeRpc(CordaRPCOps::nodeInfo),
Permissions.invokeRpc(CordaRPCOps::networkMapFeed),
Permissions.invokeRpc(CordaRPCOps::networkMapSnapshot),
Permissions.invokeRpc(CordaRPCOps::notaryIdentities),
Permissions.invokeRpc(CordaRPCOps::stateMachinesFeed),
Permissions.invokeRpc(CordaRPCOps::stateMachineRecordedTransactionMappingFeed),
Permissions.invokeRpc(CordaRPCOps::nodeInfoFromParty),
Permissions.invokeRpc(CordaRPCOps::internalVerifiedTransactionsFeed),
Permissions.invokeRpc("vaultQueryBy"),
Permissions.invokeRpc("vaultTrackBy"),
Permissions.invokeRpc(CordaRPCOps::registeredFlows)
)
private fun <A> oneOf(array: Array<A>) = array[Random().nextInt(array.size)]
private fun startInProcessNode(
executorService: ScheduledExecutorService,
nodeConf: NodeConfiguration,
config: Config,
cordappPackages: List<String>
): CordaFuture<Pair<StartedNode<Node>, Thread>> {
return executorService.fork {
log.info("Starting in-process Node ${nodeConf.myLegalName.organisation}")
// Write node.conf
writeConfig(nodeConf.baseDirectory, "node.conf", config)
// TODO pass the version in?
val node = InProcessNode(nodeConf, MOCK_VERSION_INFO, cordappPackages).start()
val nodeThread = thread(name = nodeConf.myLegalName.organisation) {
node.internals.run()
}
node to nodeThread
}.flatMap {
nodeAndThread -> addressMustBeBoundFuture(executorService, nodeConf.p2pAddress).map { nodeAndThread }
}
}
private fun startOutOfProcessNode(
nodeConf: NodeConfiguration,
config: Config,
quasarJarPath: String,
debugPort: Int?,
overriddenSystemProperties: Map<String, String>,
cordappPackages: List<String>,
maximumHeapSize: String
): Process {
log.info("Starting out-of-process Node ${nodeConf.myLegalName.organisation}, debug port is " + (debugPort ?: "not enabled"))
// Write node.conf
writeConfig(nodeConf.baseDirectory, "node.conf", config)
val systemProperties = overriddenSystemProperties + mapOf(
"name" to nodeConf.myLegalName,
"visualvm.display.name" to "corda-${nodeConf.myLegalName}",
Node.scanPackagesSystemProperty to cordappPackages.joinToString(Node.scanPackagesSeparator),
"java.io.tmpdir" to System.getProperty("java.io.tmpdir"), // Inherit from parent process
"log4j2.debug" to if(debugPort != null) "true" else "false"
)
// See experimental/quasar-hook/README.md for how to generate.
val excludePattern = "x(antlr**;bftsmart**;ch**;co.paralleluniverse**;com.codahale**;com.esotericsoftware**;" +
"com.fasterxml**;com.google**;com.ibm**;com.intellij**;com.jcabi**;com.nhaarman**;com.opengamma**;" +
"com.typesafe**;com.zaxxer**;de.javakaffee**;groovy**;groovyjarjarantlr**;groovyjarjarasm**;io.atomix**;" +
"io.github**;io.netty**;jdk**;joptsimple**;junit**;kotlin**;net.bytebuddy**;net.i2p**;org.apache**;" +
"org.assertj**;org.bouncycastle**;org.codehaus**;org.crsh**;org.dom4j**;org.fusesource**;org.h2**;" +
"org.hamcrest**;org.hibernate**;org.jboss**;org.jcp**;org.joda**;org.junit**;org.mockito**;org.objectweb**;" +
"org.objenesis**;org.slf4j**;org.w3c**;org.xml**;org.yaml**;reflectasm**;rx**)"
val extraJvmArguments = systemProperties.removeResolvedClasspath().map { "-D${it.key}=${it.value}" } +
"-javaagent:$quasarJarPath=$excludePattern"
val loggingLevel = if (debugPort == null) "INFO" else "DEBUG"
return ProcessUtilities.startCordaProcess(
className = "net.corda.node.Corda", // cannot directly get class for this, so just use string
arguments = listOf(
"--base-directory=${nodeConf.baseDirectory}",
"--logging-level=$loggingLevel",
"--no-local-shell"
),
jdwpPort = debugPort,
extraJvmArguments = extraJvmArguments,
errorLogPath = nodeConf.baseDirectory / NodeStartup.LOGS_DIRECTORY_NAME / "error.log",
workingDirectory = nodeConf.baseDirectory,
maximumHeapSize = maximumHeapSize
)
}
private fun startWebserver(handle: NodeHandle, debugPort: Int?, maximumHeapSize: String): Process {
val className = "net.corda.webserver.WebServer"
return ProcessUtilities.startCordaProcess(
className = className, // cannot directly get class for this, so just use string
arguments = listOf("--base-directory", handle.configuration.baseDirectory.toString()),
jdwpPort = debugPort,
extraJvmArguments = listOf(
"-Dname=node-${handle.configuration.p2pAddress}-webserver",
"-Djava.io.tmpdir=${System.getProperty("java.io.tmpdir")}" // Inherit from parent process
),
errorLogPath = Paths.get("error.$className.log"),
workingDirectory = null,
maximumHeapSize = maximumHeapSize
)
}
/**
* Get the package of the caller to the driver so that it can be added to the list of packages the nodes will scan.
* This makes the driver automatically pick the CorDapp module that it's run from.
*
* This returns List<String> rather than String? to make it easier to bolt onto extraCordappPackagesToScan.
*/
private fun getCallerPackage(): List<String> {
val stackTrace = Throwable().stackTrace
val index = stackTrace.indexOfLast { it.className == "net.corda.testing.driver.Driver" }
// In this case we're dealing with the the RPCDriver or one of it's cousins which are internal and we don't care about them
if (index == -1) return emptyList()
val callerPackage = Class.forName(stackTrace[index + 1].className).`package` ?:
throw IllegalStateException("Function instantiating driver must be defined in a package.")
return listOf(callerPackage.name)
}
/**
* We have an alternative way of specifying classpath for spawned process: by using "-cp" option. So duplicating the setting of this
* rather long string is un-necessary and can be harmful on Windows.
*/
private fun Map<String, Any>.removeResolvedClasspath(): Map<String, Any> {
return filterNot { it.key == "java.class.path" }
}
}
}
interface InternalDriverDSL : DriverDSL, CordformContext {
private companion object {
private val DEFAULT_POLL_INTERVAL = 500.millis
private const val DEFAULT_WARN_COUNT = 120
}
val shutdownManager: ShutdownManager
override fun baseDirectory(nodeName: String): Path = baseDirectory(CordaX500Name.parse(nodeName))
/**
* Polls a function until it returns a non-null value. Note that there is no timeout on the polling.
*
* @param pollName A description of what is being polled.
* @param pollInterval The interval of polling.
* @param warnCount The number of polls after the Driver gives a warning.
* @param check The function being polled.
* @return A future that completes with the non-null value [check] has returned.
*/
fun <A> pollUntilNonNull(pollName: String, pollInterval: Duration = DEFAULT_POLL_INTERVAL, warnCount: Int = DEFAULT_WARN_COUNT, check: () -> A?): CordaFuture<A>
/**
* Polls the given function until it returns true.
* @see pollUntilNonNull
*/
fun pollUntilTrue(pollName: String, pollInterval: Duration = DEFAULT_POLL_INTERVAL, warnCount: Int = DEFAULT_WARN_COUNT, check: () -> Boolean): CordaFuture<Unit> {
return pollUntilNonNull(pollName, pollInterval, warnCount) { if (check()) Unit else null }
}
fun start()
fun shutdown()
}
/**
* This is a helper method to allow extending of the DSL, along the lines of
* interface SomeOtherExposedDSLInterface : DriverDSL
* interface SomeOtherInternalDSLInterface : InternalDriverDSL, SomeOtherExposedDSLInterface
* class SomeOtherDSL(val driverDSL : DriverDSLImpl) : InternalDriverDSL by driverDSL, SomeOtherInternalDSLInterface
*
* @param coerce We need this explicit coercion witness because we can't put an extra DI : D bound in a `where` clause.
*/
fun <DI : DriverDSL, D : InternalDriverDSL, A> genericDriver(
driverDsl: D,
initialiseSerialization: Boolean = true,
coerce: (D) -> DI,
dsl: DI.() -> A
): A {
val serializationEnv = setGlobalSerialization(initialiseSerialization)
val shutdownHook = addShutdownHook(driverDsl::shutdown)
try {
driverDsl.start()
return dsl(coerce(driverDsl))
} catch (exception: Throwable) {
DriverDSLImpl.log.error("Driver shutting down because of exception", exception)
throw exception
} finally {
driverDsl.shutdown()
shutdownHook.cancel()
serializationEnv.unset()
}
}
/**
* This is a helper method to allow extending of the DSL, along the lines of
* interface SomeOtherExposedDSLInterface : DriverDSL
* interface SomeOtherInternalDSLInterface : InternalDriverDSL, SomeOtherExposedDSLInterface
* class SomeOtherDSL(val driverDSL : DriverDSLImpl) : InternalDriverDSL by driverDSL, SomeOtherInternalDSLInterface
*
* @param coerce We need this explicit coercion witness because we can't put an extra DI : D bound in a `where` clause.
*/
fun <DI : DriverDSL, D : InternalDriverDSL, A> genericDriver(
defaultParameters: DriverParameters = DriverParameters(),
isDebug: Boolean = defaultParameters.isDebug,
driverDirectory: Path = defaultParameters.driverDirectory,
portAllocation: PortAllocation = defaultParameters.portAllocation,
debugPortAllocation: PortAllocation = defaultParameters.debugPortAllocation,
systemProperties: Map<String, String> = defaultParameters.systemProperties,
useTestClock: Boolean = defaultParameters.useTestClock,
initialiseSerialization: Boolean = defaultParameters.initialiseSerialization,
waitForNodesToFinish: Boolean = defaultParameters.waitForAllNodesToFinish,
startNodesInProcess: Boolean = defaultParameters.startNodesInProcess,
notarySpecs: List<NotarySpec>,
extraCordappPackagesToScan: List<String> = defaultParameters.extraCordappPackagesToScan,
driverDslWrapper: (DriverDSLImpl) -> D,
coerce: (D) -> DI, dsl: DI.() -> A
): A {
val serializationEnv = setGlobalSerialization(initialiseSerialization)
val driverDsl = driverDslWrapper(
DriverDSLImpl(
portAllocation = portAllocation,
debugPortAllocation = debugPortAllocation,
systemProperties = systemProperties,
driverDirectory = driverDirectory.toAbsolutePath(),
useTestClock = useTestClock,
isDebug = isDebug,
startNodesInProcess = startNodesInProcess,
waitForNodesToFinish = waitForNodesToFinish,
extraCordappPackagesToScan = extraCordappPackagesToScan,
notarySpecs = notarySpecs
)
)
val shutdownHook = addShutdownHook(driverDsl::shutdown)
try {
driverDsl.start()
return dsl(coerce(driverDsl))
} catch (exception: Throwable) {
DriverDSLImpl.log.error("Driver shutting down because of exception", exception)
throw exception
} finally {
driverDsl.shutdown()
shutdownHook.cancel()
serializationEnv.unset()
}
}
fun getTimestampAsDirectoryName(): String {
return DateTimeFormatter.ofPattern("yyyyMMddHHmmss").withZone(ZoneOffset.UTC).format(Instant.now())
}
fun writeConfig(path: Path, filename: String, config: Config) {
val configString = config.root().render(ConfigRenderOptions.defaults())
configString.byteInputStream().copyTo(path / filename, StandardCopyOption.REPLACE_EXISTING)
}

View File

@ -0,0 +1,93 @@
package net.corda.testing.internal
import net.corda.core.CordaException
import net.corda.core.concurrent.CordaFuture
import net.corda.core.internal.concurrent.openFuture
import net.corda.core.internal.times
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.millis
import net.corda.core.utilities.seconds
import org.slf4j.LoggerFactory
import java.net.Socket
import java.net.SocketException
import java.time.Duration
import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.TimeUnit
private val log = LoggerFactory.getLogger("net.corda.testing.internal.InternalTestUtils")
/**
* @throws ListenProcessDeathException if [listenProcess] dies before the check succeeds, i.e. the check can't succeed as intended.
*/
fun addressMustBeBound(executorService: ScheduledExecutorService, hostAndPort: NetworkHostAndPort, listenProcess: Process? = null) {
addressMustBeBoundFuture(executorService, hostAndPort, listenProcess).getOrThrow()
}
fun addressMustBeBoundFuture(executorService: ScheduledExecutorService, hostAndPort: NetworkHostAndPort, listenProcess: Process? = null): CordaFuture<Unit> {
return poll(executorService, "address $hostAndPort to bind") {
if (listenProcess != null && !listenProcess.isAlive) {
throw ListenProcessDeathException(hostAndPort, listenProcess)
}
try {
Socket(hostAndPort.host, hostAndPort.port).close()
Unit
} catch (_exception: SocketException) {
null
}
}
}
/*
* The default timeout value of 40 seconds have been chosen based on previous node shutdown time estimate.
* It's been observed that nodes can take up to 30 seconds to shut down, so just to stay on the safe side the 60 seconds
* timeout has been chosen.
*/
fun addressMustNotBeBound(executorService: ScheduledExecutorService, hostAndPort: NetworkHostAndPort, timeout: Duration = 40.seconds) {
addressMustNotBeBoundFuture(executorService, hostAndPort).getOrThrow(timeout)
}
fun addressMustNotBeBoundFuture(executorService: ScheduledExecutorService, hostAndPort: NetworkHostAndPort): CordaFuture<Unit> {
return poll(executorService, "address $hostAndPort to unbind") {
try {
Socket(hostAndPort.host, hostAndPort.port).close()
null
} catch (_exception: SocketException) {
Unit
}
}
}
fun <A> poll(
executorService: ScheduledExecutorService,
pollName: String,
pollInterval: Duration = 500.millis,
warnCount: Int = 120,
check: () -> A?
): CordaFuture<A> {
val resultFuture = openFuture<A>()
val task = object : Runnable {
var counter = -1
override fun run() {
if (resultFuture.isCancelled) return // Give up, caller can no longer get the result.
if (++counter == warnCount) {
log.warn("Been polling $pollName for ${(pollInterval * warnCount.toLong()).seconds} seconds...")
}
try {
val checkResult = check()
if (checkResult != null) {
resultFuture.set(checkResult)
} else {
executorService.schedule(this, pollInterval.toMillis(), TimeUnit.MILLISECONDS)
}
} catch (t: Throwable) {
resultFuture.setException(t)
}
}
}
executorService.submit(task) // The check may be expensive, so always run it in the background even the first time.
return resultFuture
}
class ListenProcessDeathException(hostAndPort: NetworkHostAndPort, listenProcess: Process) :
CordaException("The process that was expected to listen on $hostAndPort has died with status: ${listenProcess.exitValue()}")

View File

@ -12,13 +12,8 @@ import net.corda.node.internal.Node
import net.corda.node.internal.StartedNode import net.corda.node.internal.StartedNode
import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.internal.cordapp.CordappLoader
import net.corda.node.services.config.* import net.corda.node.services.config.*
import net.corda.node.services.config.ConfigHelper
import net.corda.node.services.config.configOf
import net.corda.node.services.config.parseAsNodeConfiguration
import net.corda.node.services.config.plus
import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.config.User
import net.corda.testing.SerializationEnvironmentRule import net.corda.testing.SerializationEnvironmentRule
import net.corda.testing.driver.addressMustNotBeBoundFuture
import net.corda.testing.getFreeLocalPorts import net.corda.testing.getFreeLocalPorts
import net.corda.testing.node.MockServices.Companion.MOCK_VERSION_INFO import net.corda.testing.node.MockServices.Companion.MOCK_VERSION_INFO
import org.apache.logging.log4j.Level import org.apache.logging.log4j.Level

View File

@ -52,159 +52,25 @@ import java.nio.file.Path
import java.nio.file.Paths import java.nio.file.Paths
import java.util.* import java.util.*
interface RPCDriverExposedDSLInterface : DriverDSLExposedInterface { inline fun <reified I : RPCOps> RPCDriverDSL.startInVmRpcClient(
/**
* Starts an In-VM RPC server. Note that only a single one may be started.
*
* @param rpcUser The single user who can access the server through RPC, and their permissions.
* @param nodeLegalName The legal name of the node to check against to authenticate a super user.
* @param configuration The RPC server configuration.
* @param ops The server-side implementation of the RPC interface.
*/
fun <I : RPCOps> startInVmRpcServer(
rpcUser: User = rpcTestUser,
nodeLegalName: CordaX500Name = fakeNodeLegalName,
maxFileSize: Int = ArtemisMessagingServer.MAX_FILE_SIZE,
maxBufferedBytesPerClient: Long = 10L * ArtemisMessagingServer.MAX_FILE_SIZE,
configuration: RPCServerConfiguration = RPCServerConfiguration.default,
ops: I
): CordaFuture<RpcServerHandle>
/**
* Starts an In-VM RPC client.
*
* @param rpcOpsClass The [Class] of the RPC interface.
* @param username The username to authenticate with.
* @param password The password to authenticate with.
* @param configuration The RPC client configuration.
*/
fun <I : RPCOps> startInVmRpcClient(
rpcOpsClass: Class<I>,
username: String = rpcTestUser.username,
password: String = rpcTestUser.password,
configuration: RPCClientConfiguration = RPCClientConfiguration.default
): CordaFuture<I>
/**
* Starts an In-VM Artemis session connecting to the RPC server.
*
* @param username The username to authenticate with.
* @param password The password to authenticate with.
*/
fun startInVmArtemisSession(
username: String = rpcTestUser.username,
password: String = rpcTestUser.password
): ClientSession
/**
* Starts a Netty RPC server.
*
* @param serverName The name of the server, to be used for the folder created for Artemis files.
* @param rpcUser The single user who can access the server through RPC, and their permissions.
* @param nodeLegalName The legal name of the node to check against to authenticate a super user.
* @param configuration The RPC server configuration.
* @param ops The server-side implementation of the RPC interface.
*/
fun <I : RPCOps> startRpcServer(
serverName: String = "driver-rpc-server-${random63BitValue()}",
rpcUser: User = rpcTestUser,
nodeLegalName: CordaX500Name = fakeNodeLegalName,
maxFileSize: Int = ArtemisMessagingServer.MAX_FILE_SIZE,
maxBufferedBytesPerClient: Long = 10L * ArtemisMessagingServer.MAX_FILE_SIZE,
configuration: RPCServerConfiguration = RPCServerConfiguration.default,
customPort: NetworkHostAndPort? = null,
ops: I
): CordaFuture<RpcServerHandle>
/**
* Starts a Netty RPC client.
*
* @param rpcOpsClass The [Class] of the RPC interface.
* @param rpcAddress The address of the RPC server to connect to.
* @param username The username to authenticate with.
* @param password The password to authenticate with.
* @param configuration The RPC client configuration.
*/
fun <I : RPCOps> startRpcClient(
rpcOpsClass: Class<I>,
rpcAddress: NetworkHostAndPort,
username: String = rpcTestUser.username,
password: String = rpcTestUser.password,
configuration: RPCClientConfiguration = RPCClientConfiguration.default
): CordaFuture<I>
/**
* Starts a Netty RPC client in a new JVM process that calls random RPCs with random arguments.
*
* @param rpcOpsClass The [Class] of the RPC interface.
* @param rpcAddress The address of the RPC server to connect to.
* @param username The username to authenticate with.
* @param password The password to authenticate with.
*/
fun <I : RPCOps> startRandomRpcClient(
rpcOpsClass: Class<I>,
rpcAddress: NetworkHostAndPort,
username: String = rpcTestUser.username,
password: String = rpcTestUser.password
): CordaFuture<Process>
/**
* Starts a Netty Artemis session connecting to an RPC server.
*
* @param rpcAddress The address of the RPC server.
* @param username The username to authenticate with.
* @param password The password to authenticate with.
*/
fun startArtemisSession(
rpcAddress: NetworkHostAndPort,
username: String = rpcTestUser.username,
password: String = rpcTestUser.password
): ClientSession
fun startRpcBroker(
serverName: String = "driver-rpc-server-${random63BitValue()}",
rpcUser: User = rpcTestUser,
maxFileSize: Int = ArtemisMessagingServer.MAX_FILE_SIZE,
maxBufferedBytesPerClient: Long = 10L * ArtemisMessagingServer.MAX_FILE_SIZE,
customPort: NetworkHostAndPort? = null
): CordaFuture<RpcBrokerHandle>
fun startInVmRpcBroker(
rpcUser: User = rpcTestUser,
maxFileSize: Int = ArtemisMessagingServer.MAX_FILE_SIZE,
maxBufferedBytesPerClient: Long = 10L * ArtemisMessagingServer.MAX_FILE_SIZE
): CordaFuture<RpcBrokerHandle>
fun <I : RPCOps> startRpcServerWithBrokerRunning(
rpcUser: User = rpcTestUser,
nodeLegalName: CordaX500Name = fakeNodeLegalName,
configuration: RPCServerConfiguration = RPCServerConfiguration.default,
ops: I,
brokerHandle: RpcBrokerHandle
): RpcServerHandle
}
inline fun <reified I : RPCOps> RPCDriverExposedDSLInterface.startInVmRpcClient(
username: String = rpcTestUser.username, username: String = rpcTestUser.username,
password: String = rpcTestUser.password, password: String = rpcTestUser.password,
configuration: RPCClientConfiguration = RPCClientConfiguration.default configuration: RPCClientConfiguration = RPCClientConfiguration.default
) = startInVmRpcClient(I::class.java, username, password, configuration) ) = startInVmRpcClient(I::class.java, username, password, configuration)
inline fun <reified I : RPCOps> RPCDriverExposedDSLInterface.startRandomRpcClient( inline fun <reified I : RPCOps> RPCDriverDSL.startRandomRpcClient(
hostAndPort: NetworkHostAndPort, hostAndPort: NetworkHostAndPort,
username: String = rpcTestUser.username, username: String = rpcTestUser.username,
password: String = rpcTestUser.password password: String = rpcTestUser.password
) = startRandomRpcClient(I::class.java, hostAndPort, username, password) ) = startRandomRpcClient(I::class.java, hostAndPort, username, password)
inline fun <reified I : RPCOps> RPCDriverExposedDSLInterface.startRpcClient( inline fun <reified I : RPCOps> RPCDriverDSL.startRpcClient(
rpcAddress: NetworkHostAndPort, rpcAddress: NetworkHostAndPort,
username: String = rpcTestUser.username, username: String = rpcTestUser.username,
password: String = rpcTestUser.password, password: String = rpcTestUser.password,
configuration: RPCClientConfiguration = RPCClientConfiguration.default configuration: RPCClientConfiguration = RPCClientConfiguration.default
) = startRpcClient(I::class.java, rpcAddress, username, password, configuration) ) = startRpcClient(I::class.java, rpcAddress, username, password, configuration)
interface RPCDriverInternalDSLInterface : DriverDSLInternalInterface, RPCDriverExposedDSLInterface
data class RpcBrokerHandle( data class RpcBrokerHandle(
val hostAndPort: NetworkHostAndPort?, val hostAndPort: NetworkHostAndPort?,
/** null if this is an InVM broker */ /** null if this is an InVM broker */
@ -235,26 +101,28 @@ fun <A> rpcDriver(
extraCordappPackagesToScan: List<String> = emptyList(), extraCordappPackagesToScan: List<String> = emptyList(),
notarySpecs: List<NotarySpec> = emptyList(), notarySpecs: List<NotarySpec> = emptyList(),
externalTrace: Trace? = null, externalTrace: Trace? = null,
dsl: RPCDriverExposedDSLInterface.() -> A dsl: RPCDriverDSL.() -> A
) = genericDriver( ): A {
driverDsl = RPCDriverDSL( return genericDriver(
DriverDSL( driverDsl = RPCDriverDSL(
portAllocation = portAllocation, DriverDSLImpl(
debugPortAllocation = debugPortAllocation, portAllocation = portAllocation,
systemProperties = systemProperties, debugPortAllocation = debugPortAllocation,
driverDirectory = driverDirectory.toAbsolutePath(), systemProperties = systemProperties,
useTestClock = useTestClock, driverDirectory = driverDirectory.toAbsolutePath(),
isDebug = isDebug, useTestClock = useTestClock,
startNodesInProcess = startNodesInProcess, isDebug = isDebug,
waitForNodesToFinish = waitForNodesToFinish, startNodesInProcess = startNodesInProcess,
extraCordappPackagesToScan = extraCordappPackagesToScan, waitForNodesToFinish = waitForNodesToFinish,
notarySpecs = notarySpecs extraCordappPackagesToScan = extraCordappPackagesToScan,
), externalTrace notarySpecs = notarySpecs
), ), externalTrace
coerce = { it }, ),
dsl = dsl, coerce = { it },
initialiseSerialization = false dsl = dsl,
) initialiseSerialization = false
)
}
private class SingleUserSecurityManager(val rpcUser: User) : ActiveMQSecurityManager3 { private class SingleUserSecurityManager(val rpcUser: User) : ActiveMQSecurityManager3 {
override fun validateUser(user: String?, password: String?) = isValid(user, password) override fun validateUser(user: String?, password: String?) = isValid(user, password)
@ -276,8 +144,8 @@ private class SingleUserSecurityManager(val rpcUser: User) : ActiveMQSecurityMan
} }
data class RPCDriverDSL( data class RPCDriverDSL(
private val driverDSL: DriverDSL, private val externalTrace: Trace? private val driverDSL: DriverDSLImpl, private val externalTrace: Trace?
) : DriverDSLInternalInterface by driverDSL, RPCDriverInternalDSLInterface { ) : InternalDriverDSL by driverDSL {
private companion object { private companion object {
val notificationAddress = "notifications" val notificationAddress = "notifications"
@ -340,12 +208,20 @@ data class RPCDriverDSL(
} }
} }
override fun <I : RPCOps> startInVmRpcServer( /**
rpcUser: User, * Starts an In-VM RPC server. Note that only a single one may be started.
nodeLegalName: CordaX500Name, *
maxFileSize: Int, * @param rpcUser The single user who can access the server through RPC, and their permissions.
maxBufferedBytesPerClient: Long, * @param nodeLegalName The legal name of the node to check against to authenticate a super user.
configuration: RPCServerConfiguration, * @param configuration The RPC server configuration.
* @param ops The server-side implementation of the RPC interface.
*/
fun <I : RPCOps> startInVmRpcServer(
rpcUser: User = rpcTestUser,
nodeLegalName: CordaX500Name = fakeNodeLegalName,
maxFileSize: Int = ArtemisMessagingServer.MAX_FILE_SIZE,
maxBufferedBytesPerClient: Long = 10L * ArtemisMessagingServer.MAX_FILE_SIZE,
configuration: RPCServerConfiguration = RPCServerConfiguration.default,
ops: I ops: I
): CordaFuture<RpcServerHandle> { ): CordaFuture<RpcServerHandle> {
return startInVmRpcBroker(rpcUser, maxFileSize, maxBufferedBytesPerClient).map { broker -> return startInVmRpcBroker(rpcUser, maxFileSize, maxBufferedBytesPerClient).map { broker ->
@ -353,7 +229,20 @@ data class RPCDriverDSL(
} }
} }
override fun <I : RPCOps> startInVmRpcClient(rpcOpsClass: Class<I>, username: String, password: String, configuration: RPCClientConfiguration): CordaFuture<I> { /**
* Starts an In-VM RPC client.
*
* @param rpcOpsClass The [Class] of the RPC interface.
* @param username The username to authenticate with.
* @param password The password to authenticate with.
* @param configuration The RPC client configuration.
*/
fun <I : RPCOps> startInVmRpcClient(
rpcOpsClass: Class<I>,
username: String = rpcTestUser.username,
password: String = rpcTestUser.password,
configuration: RPCClientConfiguration = RPCClientConfiguration.default
): CordaFuture<I> {
return driverDSL.executorService.fork { return driverDSL.executorService.fork {
val client = RPCClient<I>(inVmClientTransportConfiguration, configuration) val client = RPCClient<I>(inVmClientTransportConfiguration, configuration)
val connection = client.start(rpcOpsClass, username, password, externalTrace) val connection = client.start(rpcOpsClass, username, password, externalTrace)
@ -364,7 +253,16 @@ data class RPCDriverDSL(
} }
} }
override fun startInVmArtemisSession(username: String, password: String): ClientSession { /**
* Starts an In-VM Artemis session connecting to the RPC server.
*
* @param username The username to authenticate with.
* @param password The password to authenticate with.
*/
fun startInVmArtemisSession(
username: String = rpcTestUser.username,
password: String = rpcTestUser.password
): ClientSession {
val locator = ActiveMQClient.createServerLocatorWithoutHA(inVmClientTransportConfiguration) val locator = ActiveMQClient.createServerLocatorWithoutHA(inVmClientTransportConfiguration)
val sessionFactory = locator.createSessionFactory() val sessionFactory = locator.createSessionFactory()
val session = sessionFactory.createSession(username, password, false, true, true, locator.isPreAcknowledge, DEFAULT_ACK_BATCH_SIZE) val session = sessionFactory.createSession(username, password, false, true, true, locator.isPreAcknowledge, DEFAULT_ACK_BATCH_SIZE)
@ -376,14 +274,23 @@ data class RPCDriverDSL(
return session return session
} }
override fun <I : RPCOps> startRpcServer( /**
serverName: String, * Starts a Netty RPC server.
rpcUser: User, *
nodeLegalName: CordaX500Name, * @param serverName The name of the server, to be used for the folder created for Artemis files.
maxFileSize: Int, * @param rpcUser The single user who can access the server through RPC, and their permissions.
maxBufferedBytesPerClient: Long, * @param nodeLegalName The legal name of the node to check against to authenticate a super user.
configuration: RPCServerConfiguration, * @param configuration The RPC server configuration.
customPort: NetworkHostAndPort?, * @param ops The server-side implementation of the RPC interface.
*/
fun <I : RPCOps> startRpcServer(
serverName: String = "driver-rpc-server-${random63BitValue()}",
rpcUser: User = rpcTestUser,
nodeLegalName: CordaX500Name = fakeNodeLegalName,
maxFileSize: Int = ArtemisMessagingServer.MAX_FILE_SIZE,
maxBufferedBytesPerClient: Long = 10L * ArtemisMessagingServer.MAX_FILE_SIZE,
configuration: RPCServerConfiguration = RPCServerConfiguration.default,
customPort: NetworkHostAndPort? = null,
ops: I ops: I
): CordaFuture<RpcServerHandle> { ): CordaFuture<RpcServerHandle> {
return startRpcBroker(serverName, rpcUser, maxFileSize, maxBufferedBytesPerClient, customPort).map { broker -> return startRpcBroker(serverName, rpcUser, maxFileSize, maxBufferedBytesPerClient, customPort).map { broker ->
@ -391,12 +298,21 @@ data class RPCDriverDSL(
} }
} }
override fun <I : RPCOps> startRpcClient( /**
* Starts a Netty RPC client.
*
* @param rpcOpsClass The [Class] of the RPC interface.
* @param rpcAddress The address of the RPC server to connect to.
* @param username The username to authenticate with.
* @param password The password to authenticate with.
* @param configuration The RPC client configuration.
*/
fun <I : RPCOps> startRpcClient(
rpcOpsClass: Class<I>, rpcOpsClass: Class<I>,
rpcAddress: NetworkHostAndPort, rpcAddress: NetworkHostAndPort,
username: String, username: String = rpcTestUser.username,
password: String, password: String = rpcTestUser.password,
configuration: RPCClientConfiguration configuration: RPCClientConfiguration = RPCClientConfiguration.default
): CordaFuture<I> { ): CordaFuture<I> {
return driverDSL.executorService.fork { return driverDSL.executorService.fork {
val client = RPCClient<I>(ArtemisTcpTransport.tcpTransport(ConnectionDirection.Outbound(), rpcAddress, null), configuration) val client = RPCClient<I>(ArtemisTcpTransport.tcpTransport(ConnectionDirection.Outbound(), rpcAddress, null), configuration)
@ -408,13 +324,37 @@ data class RPCDriverDSL(
} }
} }
override fun <I : RPCOps> startRandomRpcClient(rpcOpsClass: Class<I>, rpcAddress: NetworkHostAndPort, username: String, password: String): CordaFuture<Process> { /**
* Starts a Netty RPC client in a new JVM process that calls random RPCs with random arguments.
*
* @param rpcOpsClass The [Class] of the RPC interface.
* @param rpcAddress The address of the RPC server to connect to.
* @param username The username to authenticate with.
* @param password The password to authenticate with.
*/
fun <I : RPCOps> startRandomRpcClient(
rpcOpsClass: Class<I>,
rpcAddress: NetworkHostAndPort,
username: String = rpcTestUser.username,
password: String = rpcTestUser.password
): CordaFuture<Process> {
val process = ProcessUtilities.startJavaProcess<RandomRpcUser>(listOf(rpcOpsClass.name, rpcAddress.toString(), username, password)) val process = ProcessUtilities.startJavaProcess<RandomRpcUser>(listOf(rpcOpsClass.name, rpcAddress.toString(), username, password))
driverDSL.shutdownManager.registerProcessShutdown(process) driverDSL.shutdownManager.registerProcessShutdown(process)
return doneFuture(process) return doneFuture(process)
} }
override fun startArtemisSession(rpcAddress: NetworkHostAndPort, username: String, password: String): ClientSession { /**
* Starts a Netty Artemis session connecting to an RPC server.
*
* @param rpcAddress The address of the RPC server.
* @param username The username to authenticate with.
* @param password The password to authenticate with.
*/
fun startArtemisSession(
rpcAddress: NetworkHostAndPort,
username: String = rpcTestUser.username,
password: String = rpcTestUser.password
): ClientSession {
val locator = ActiveMQClient.createServerLocatorWithoutHA(createNettyClientTransportConfiguration(rpcAddress)) val locator = ActiveMQClient.createServerLocatorWithoutHA(createNettyClientTransportConfiguration(rpcAddress))
val sessionFactory = locator.createSessionFactory() val sessionFactory = locator.createSessionFactory()
val session = sessionFactory.createSession(username, password, false, true, true, false, DEFAULT_ACK_BATCH_SIZE) val session = sessionFactory.createSession(username, password, false, true, true, false, DEFAULT_ACK_BATCH_SIZE)
@ -427,12 +367,12 @@ data class RPCDriverDSL(
return session return session
} }
override fun startRpcBroker( fun startRpcBroker(
serverName: String, serverName: String = "driver-rpc-server-${random63BitValue()}",
rpcUser: User, rpcUser: User = rpcTestUser,
maxFileSize: Int, maxFileSize: Int = ArtemisMessagingServer.MAX_FILE_SIZE,
maxBufferedBytesPerClient: Long, maxBufferedBytesPerClient: Long = 10L * ArtemisMessagingServer.MAX_FILE_SIZE,
customPort: NetworkHostAndPort? customPort: NetworkHostAndPort? = null
): CordaFuture<RpcBrokerHandle> { ): CordaFuture<RpcBrokerHandle> {
val hostAndPort = customPort ?: driverDSL.portAllocation.nextHostAndPort() val hostAndPort = customPort ?: driverDSL.portAllocation.nextHostAndPort()
addressMustNotBeBound(driverDSL.executorService, hostAndPort) addressMustNotBeBound(driverDSL.executorService, hostAndPort)
@ -452,7 +392,11 @@ data class RPCDriverDSL(
} }
} }
override fun startInVmRpcBroker(rpcUser: User, maxFileSize: Int, maxBufferedBytesPerClient: Long): CordaFuture<RpcBrokerHandle> { fun startInVmRpcBroker(
rpcUser: User = rpcTestUser,
maxFileSize: Int = ArtemisMessagingServer.MAX_FILE_SIZE,
maxBufferedBytesPerClient: Long = 10L * ArtemisMessagingServer.MAX_FILE_SIZE
): CordaFuture<RpcBrokerHandle> {
return driverDSL.executorService.fork { return driverDSL.executorService.fork {
val artemisConfig = createInVmRpcServerArtemisConfig(maxFileSize, maxBufferedBytesPerClient) val artemisConfig = createInVmRpcServerArtemisConfig(maxFileSize, maxBufferedBytesPerClient)
val server = EmbeddedActiveMQ() val server = EmbeddedActiveMQ()
@ -471,10 +415,10 @@ data class RPCDriverDSL(
} }
} }
override fun <I : RPCOps> startRpcServerWithBrokerRunning( fun <I : RPCOps> startRpcServerWithBrokerRunning(
rpcUser: User, rpcUser: User = rpcTestUser,
nodeLegalName: CordaX500Name, nodeLegalName: CordaX500Name = fakeNodeLegalName,
configuration: RPCServerConfiguration, configuration: RPCServerConfiguration = RPCServerConfiguration.default,
ops: I, ops: I,
brokerHandle: RpcBrokerHandle brokerHandle: RpcBrokerHandle
): RpcServerHandle { ): RpcServerHandle {

View File

@ -1,4 +1,4 @@
package net.corda.testing.driver package net.corda.testing.internal
import net.corda.core.concurrent.CordaFuture import net.corda.core.concurrent.CordaFuture
import net.corda.core.internal.ThreadBox import net.corda.core.internal.ThreadBox

View File

@ -8,7 +8,7 @@ import net.corda.core.internal.concurrent.flatMap
import net.corda.core.internal.concurrent.transpose import net.corda.core.internal.concurrent.transpose
import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.getOrThrow
import net.corda.testing.driver.DriverDSL import net.corda.testing.internal.DriverDSLImpl
import net.corda.testing.driver.PortAllocation import net.corda.testing.driver.PortAllocation
import net.corda.testing.driver.driver import net.corda.testing.driver.driver
@ -49,8 +49,8 @@ private fun CordformDefinition.runNodes(waitForAllNodesToFinish: Boolean, block:
portAllocation = PortAllocation.Incremental(maxPort + 1), portAllocation = PortAllocation.Incremental(maxPort + 1),
waitForAllNodesToFinish = waitForAllNodesToFinish waitForAllNodesToFinish = waitForAllNodesToFinish
) { ) {
this as DriverDSLImpl // access internal API
setup(this) setup(this)
this as DriverDSL // startCordformNode is an internal API
nodes.map { nodes.map {
val startedNode = startCordformNode(it) val startedNode = startCordformNode(it)
if (it.webAddress != null) { if (it.webAddress != null) {

View File

@ -3,7 +3,7 @@ package net.corda.testing.internal.performance
import com.codahale.metrics.Gauge import com.codahale.metrics.Gauge
import com.codahale.metrics.MetricRegistry import com.codahale.metrics.MetricRegistry
import com.google.common.base.Stopwatch import com.google.common.base.Stopwatch
import net.corda.testing.driver.ShutdownManager import net.corda.testing.internal.ShutdownManager
import java.time.Duration import java.time.Duration
import java.util.* import java.util.*
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch

View File

@ -3,7 +3,7 @@ package net.corda.testing.internal.performance
import com.codahale.metrics.ConsoleReporter import com.codahale.metrics.ConsoleReporter
import com.codahale.metrics.JmxReporter import com.codahale.metrics.JmxReporter
import com.codahale.metrics.MetricRegistry import com.codahale.metrics.MetricRegistry
import net.corda.testing.driver.ShutdownManager import net.corda.testing.internal.ShutdownManager
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.management.ObjectName import javax.management.ObjectName
import kotlin.concurrent.thread import kotlin.concurrent.thread

View File

@ -1,29 +0,0 @@
@file:JvmName("X500NameUtils")
package net.corda.testing
import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.asn1.x500.X500NameBuilder
import org.bouncycastle.asn1.x500.style.BCStyle
/**
* Generate a distinguished name from the provided X500 .
*
* @param O organisation name.
* @param L locality.
* @param C county.
* @param CN common name.
* @param OU organisation unit.
* @param ST state.
*/
@JvmOverloads
fun getX500Name(O: String, L: String, C: String, CN: String? = null, OU: String? = null, ST: String? = null): X500Name {
return X500NameBuilder(BCStyle.INSTANCE).apply {
addRDN(BCStyle.C, C)
ST?.let { addRDN(BCStyle.ST, it) }
addRDN(BCStyle.L, L)
addRDN(BCStyle.O, O)
OU?.let { addRDN(BCStyle.OU, it) }
CN?.let { addRDN(BCStyle.CN, it) }
}.build()
}

View File

@ -16,14 +16,16 @@ import net.corda.core.transactions.LedgerTransaction
import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.contextLogger import net.corda.core.utilities.contextLogger
import net.corda.node.services.config.configureDevKeyAndTrustStores import net.corda.node.services.config.configureDevKeyAndTrustStores
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_USER
import net.corda.nodeapi.ArtemisTcpTransport import net.corda.nodeapi.ArtemisTcpTransport
import net.corda.nodeapi.ConnectionDirection import net.corda.nodeapi.ConnectionDirection
import net.corda.nodeapi.VerifierApi import net.corda.nodeapi.VerifierApi
import net.corda.nodeapi.internal.config.NodeSSLConfiguration import net.corda.nodeapi.internal.config.NodeSSLConfiguration
import net.corda.nodeapi.internal.config.SSLConfiguration import net.corda.nodeapi.internal.config.SSLConfiguration
import net.corda.testing.driver.* import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_USER
import net.corda.testing.internal.ProcessUtilities import net.corda.testing.driver.NodeHandle
import net.corda.testing.driver.PortAllocation
import net.corda.testing.driver.driver
import net.corda.testing.internal.*
import net.corda.testing.node.NotarySpec import net.corda.testing.node.NotarySpec
import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.SimpleString
import org.apache.activemq.artemis.api.core.client.ActiveMQClient import org.apache.activemq.artemis.api.core.client.ActiveMQClient
@ -43,34 +45,6 @@ import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ScheduledExecutorService import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
/**
* This file defines an extension to [DriverDSL] that allows starting of verifier processes and
* lightweight verification requestors.
*/
interface VerifierExposedDSLInterface : DriverDSLExposedInterface {
/** Starts a lightweight verification requestor that implements the Node's Verifier API */
fun startVerificationRequestor(name: CordaX500Name): CordaFuture<VerificationRequestorHandle>
/** Starts an out of process verifier connected to [address] */
fun startVerifier(address: NetworkHostAndPort): CordaFuture<VerifierHandle>
/**
* Waits until [number] verifiers are listening for verification requests coming from the Node. Check
* [VerificationRequestorHandle.waitUntilNumberOfVerifiers] for an equivalent for requestors.
*/
fun NodeHandle.waitUntilNumberOfVerifiers(number: Int)
}
/** Starts a verifier connecting to the specified node */
fun VerifierExposedDSLInterface.startVerifier(nodeHandle: NodeHandle) =
startVerifier(nodeHandle.configuration.p2pAddress)
/** Starts a verifier connecting to the specified requestor */
fun VerifierExposedDSLInterface.startVerifier(verificationRequestorHandle: VerificationRequestorHandle) =
startVerifier(verificationRequestorHandle.p2pAddress)
interface VerifierInternalDSLInterface : DriverDSLInternalInterface, VerifierExposedDSLInterface
/** /**
* Behaves the same as [driver] and adds verifier-related functionality. * Behaves the same as [driver] and adds verifier-related functionality.
*/ */
@ -85,10 +59,10 @@ fun <A> verifierDriver(
waitForNodesToFinish: Boolean = false, waitForNodesToFinish: Boolean = false,
extraCordappPackagesToScan: List<String> = emptyList(), extraCordappPackagesToScan: List<String> = emptyList(),
notarySpecs: List<NotarySpec> = emptyList(), notarySpecs: List<NotarySpec> = emptyList(),
dsl: VerifierExposedDSLInterface.() -> A dsl: VerifierDriverDSL.() -> A
) = genericDriver( ) = genericDriver(
driverDsl = VerifierDriverDSL( driverDsl = VerifierDriverDSL(
DriverDSL( DriverDSLImpl(
portAllocation = portAllocation, portAllocation = portAllocation,
debugPortAllocation = debugPortAllocation, debugPortAllocation = debugPortAllocation,
systemProperties = systemProperties, systemProperties = systemProperties,
@ -143,10 +117,8 @@ data class VerificationRequestorHandle(
} }
data class VerifierDriverDSL( data class VerifierDriverDSL(private val driverDSL: DriverDSLImpl) : InternalDriverDSL by driverDSL {
val driverDSL: DriverDSL private val verifierCount = AtomicInteger(0)
) : DriverDSLInternalInterface by driverDSL, VerifierInternalDSLInterface {
val verifierCount = AtomicInteger(0)
companion object { companion object {
private val log = contextLogger() private val log = contextLogger()
@ -183,7 +155,8 @@ data class VerifierDriverDSL(
} }
} }
override fun startVerificationRequestor(name: CordaX500Name): CordaFuture<VerificationRequestorHandle> { /** Starts a lightweight verification requestor that implements the Node's Verifier API */
fun startVerificationRequestor(name: CordaX500Name): CordaFuture<VerificationRequestorHandle> {
val hostAndPort = driverDSL.portAllocation.nextHostAndPort() val hostAndPort = driverDSL.portAllocation.nextHostAndPort()
return driverDSL.executorService.fork { return driverDSL.executorService.fork {
startVerificationRequestorInternal(name, hostAndPort) startVerificationRequestorInternal(name, hostAndPort)
@ -255,7 +228,8 @@ data class VerifierDriverDSL(
) )
} }
override fun startVerifier(address: NetworkHostAndPort): CordaFuture<VerifierHandle> { /** Starts an out of process verifier connected to [address] */
fun startVerifier(address: NetworkHostAndPort): CordaFuture<VerifierHandle> {
log.info("Starting verifier connecting to address $address") log.info("Starting verifier connecting to address $address")
val id = verifierCount.andIncrement val id = verifierCount.andIncrement
val jdwpPort = if (driverDSL.isDebug) driverDSL.debugPortAllocation.nextPort() else null val jdwpPort = if (driverDSL.isDebug) driverDSL.debugPortAllocation.nextPort() else null
@ -270,6 +244,16 @@ data class VerifierDriverDSL(
return doneFuture(VerifierHandle(process)) return doneFuture(VerifierHandle(process))
} }
/** Starts a verifier connecting to the specified node */
fun startVerifier(nodeHandle: NodeHandle): CordaFuture<VerifierHandle> {
return startVerifier(nodeHandle.configuration.p2pAddress)
}
/** Starts a verifier connecting to the specified requestor */
fun startVerifier(verificationRequestorHandle: VerificationRequestorHandle): CordaFuture<VerifierHandle> {
return startVerifier(verificationRequestorHandle.p2pAddress)
}
private fun <A> NodeHandle.connectToNode(closure: (ClientSession) -> A): A { private fun <A> NodeHandle.connectToNode(closure: (ClientSession) -> A): A {
val transport = ArtemisTcpTransport.tcpTransport(ConnectionDirection.Outbound(), configuration.p2pAddress, configuration) val transport = ArtemisTcpTransport.tcpTransport(ConnectionDirection.Outbound(), configuration.p2pAddress, configuration)
val locator = ActiveMQClient.createServerLocatorWithoutHA(transport) val locator = ActiveMQClient.createServerLocatorWithoutHA(transport)
@ -280,7 +264,11 @@ data class VerifierDriverDSL(
} }
} }
override fun NodeHandle.waitUntilNumberOfVerifiers(number: Int) { /**
* Waits until [number] verifiers are listening for verification requests coming from the Node. Check
* [VerificationRequestorHandle.waitUntilNumberOfVerifiers] for an equivalent for requestors.
*/
fun NodeHandle.waitUntilNumberOfVerifiers(number: Int) {
connectToNode { session -> connectToNode { session ->
poll(driverDSL.executorService, "$number verifiers to come online") { poll(driverDSL.executorService, "$number verifiers to come online") {
if (session.queueQuery(SimpleString(VerifierApi.VERIFICATION_REQUESTS_QUEUE_NAME)).consumerCount >= number) { if (session.queueQuery(SimpleString(VerifierApi.VERIFICATION_REQUESTS_QUEUE_NAME)).consumerCount >= number) {

View File

@ -4,8 +4,8 @@ import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.getOrThrow
import net.corda.testing.DUMMY_BANK_A import net.corda.testing.DUMMY_BANK_A
import net.corda.testing.driver.WebserverHandle import net.corda.testing.driver.WebserverHandle
import net.corda.testing.driver.addressMustBeBound import net.corda.testing.internal.addressMustBeBound
import net.corda.testing.driver.addressMustNotBeBound import net.corda.testing.internal.addressMustNotBeBound
import net.corda.testing.driver.driver import net.corda.testing.driver.driver
import org.junit.Test import org.junit.Test
import java.util.concurrent.Executors import java.util.concurrent.Executors