Merge branch 'release/os/4.6' into os_4.6-feature_pass_in_client_id_when_starting_a_flow

This commit is contained in:
Kyriakos Tharrouniatis 2020-08-06 13:21:45 +01:00
commit 2afedeabb4
118 changed files with 3312 additions and 1138 deletions

View File

@ -53,7 +53,13 @@ pipeline {
} }
stages { stages {
/*
* Temporarily disable Sonatype checks for regression builds
*/
stage('Sonatype Check') { stage('Sonatype Check') {
when {
expression { isReleaseTag }
}
steps { steps {
sh "./gradlew --no-daemon clean jar" sh "./gradlew --no-daemon clean jar"
script { script {

View File

@ -55,7 +55,13 @@ pipeline {
} }
stages { stages {
/*
* Temporarily disable Sonatype checks for regression builds
*/
stage('Sonatype Check') { stage('Sonatype Check') {
when {
expression { isReleaseTag }
}
steps { steps {
sh "./gradlew --no-daemon clean jar" sh "./gradlew --no-daemon clean jar"
script { script {

View File

@ -13,6 +13,7 @@ see changes to this list.
* agoldvarg * agoldvarg
* Ajitha Thayaharan (BCS Technology International) * Ajitha Thayaharan (BCS Technology International)
* Alberto Arri (R3) * Alberto Arri (R3)
* Alex Karnezis
* amiracam * amiracam
* Amol Pednekar * Amol Pednekar
* Andras Slemmer (R3) * Andras Slemmer (R3)

View File

@ -1,5 +1,4 @@
import com.r3.testing.DistributeTestsBy import com.r3.testing.DistributeTestsBy
import com.r3.testing.ParallelTestGroup
import com.r3.testing.PodLogLevel import com.r3.testing.PodLogLevel
import static org.gradle.api.JavaVersion.VERSION_11 import static org.gradle.api.JavaVersion.VERSION_11
@ -172,16 +171,30 @@ buildscript {
} }
} }
} else { } else {
maven {
url "${artifactory_contextUrl}/corda-dependencies-dev"
content {
includeGroupByRegex 'net\\.corda(\\..*)?'
includeGroupByRegex 'com\\.r3(\\..*)?'
}
mavenContent {
snapshotsOnly()
}
}
maven {
url "${artifactory_contextUrl}/corda-releases"
content {
includeGroupByRegex 'net\\.corda(\\..*)?'
includeGroupByRegex 'com\\.r3(\\..*)?'
}
}
mavenCentral() mavenCentral()
jcenter() jcenter()
maven { maven {
url 'https://kotlin.bintray.com/kotlinx' url 'https://kotlin.bintray.com/kotlinx'
content {
includeGroup 'org.jetbrains.kotlin'
} }
maven {
url "${artifactory_contextUrl}/corda-dependencies-dev"
}
maven {
url "${artifactory_contextUrl}/corda-releases"
} }
} }
} }
@ -204,11 +217,13 @@ buildscript {
// Capsule gradle plugin forked and maintained locally to support Gradle 5.x // Capsule gradle plugin forked and maintained locally to support Gradle 5.x
// See https://github.com/corda/gradle-capsule-plugin // See https://github.com/corda/gradle-capsule-plugin
classpath "us.kirchmeier:gradle-capsule-plugin:1.0.4_r3" classpath "us.kirchmeier:gradle-capsule-plugin:1.0.4_r3"
classpath group: "com.r3.testing", name: "gradle-distributed-testing-plugin", version: "1.2-LOCAL-K8S-SHARED-CACHE-SNAPSHOT", changing: true classpath group: "com.r3.testing", name: "gradle-distributed-testing-plugin", version: "1.3-SNAPSHOT", changing: true
classpath group: "com.r3.dependx", name: "gradle-dependx", version: "0.1.13", changing: true
classpath "com.bmuschko:gradle-docker-plugin:5.0.0"
classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.8" classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.8"
} }
configurations.all {
resolutionStrategy.cacheChangingModulesFor 0, 'seconds'
}
} }
plugins { plugins {
@ -222,8 +237,7 @@ apply plugin: 'project-report'
apply plugin: 'com.github.ben-manes.versions' apply plugin: 'com.github.ben-manes.versions'
apply plugin: 'net.corda.plugins.publish-utils' apply plugin: 'net.corda.plugins.publish-utils'
apply plugin: 'com.jfrog.artifactory' apply plugin: 'com.jfrog.artifactory'
apply plugin: "com.bmuschko.docker-remote-api" apply plugin: 'com.r3.testing.distributed-testing'
apply plugin: "com.r3.dependx.dependxies"
// If the command line project option -PversionFromGit is added to the gradle invocation, we'll resolve // If the command line project option -PversionFromGit is added to the gradle invocation, we'll resolve
@ -390,11 +404,32 @@ allprojects {
} }
} }
} else { } else {
maven {
url "${artifactory_contextUrl}/corda-dependencies"
content {
includeGroupByRegex 'net\\.corda(\\..*)?'
includeGroupByRegex 'com\\.r3(\\..*)?'
includeGroup 'co.paralleluniverse'
includeGroup 'org.crashub'
includeGroup 'com.github.bft-smart'
}
}
maven {
url "${artifactory_contextUrl}/corda-dev"
content {
includeGroupByRegex 'net\\.corda(\\..*)?'
includeGroupByRegex 'com\\.r3(\\..*)?'
}
}
maven {
url 'https://repo.gradle.org/gradle/libs-releases'
content {
includeGroup 'org.gradle'
includeGroup 'com.github.detro'
}
}
mavenCentral() mavenCentral()
jcenter() jcenter()
maven { url "${artifactory_contextUrl}/corda-dependencies" }
maven { url 'https://repo.gradle.org/gradle/libs-releases' }
maven { url "${artifactory_contextUrl}/corda-dev" }
} }
} }
@ -655,11 +690,6 @@ artifactory {
} }
} }
dependxiesModule {
mode = "monitor"
skipTasks = "test,integrationTest,smokeTest,slowIntegrationTest"
}
tasks.register('generateApi', net.corda.plugins.apiscanner.GenerateApi) { tasks.register('generateApi', net.corda.plugins.apiscanner.GenerateApi) {
baseName = "api-corda" baseName = "api-corda"
} }
@ -705,83 +735,45 @@ buildScan {
termsOfServiceAgree = 'yes' termsOfServiceAgree = 'yes'
} }
ext.generalPurpose = [ distributedTesting {
numberOfShards: 15, profilesURL = 'https://raw.githubusercontent.com/corda/infrastructure-profiles/master'
streamOutput: false,
coresPerFork: 2,
memoryInGbPerFork: 12,
nodeTaints: "small"
]
ext.largeScaleSet = [ parallelTestGroups {
numberOfShards: 15, allParallelIntegrationTest {
streamOutput: false, testGroups 'integrationTest'
coresPerFork: 6, profile 'generalPurpose.yml'
memoryInGbPerFork: 10, podLogLevel PodLogLevel.INFO
nodeTaints: "big" distribution DistributeTestsBy.METHOD
] }
allParallelUnitTest {
podLogLevel PodLogLevel.INFO
testGroups 'test'
profile 'generalPurpose.yml'
distribution DistributeTestsBy.CLASS
}
allParallelUnitAndIntegrationTest {
testGroups 'test', 'integrationTest'
profile 'generalPurpose.yml'
distribution DistributeTestsBy.METHOD
}
parallelRegressionTest {
testGroups 'test', 'integrationTest', 'smokeTest'
profile 'generalPurpose.yml'
distribution DistributeTestsBy.METHOD
}
allParallelSmokeTest {
testGroups 'smokeTest'
profile 'generalPurpose.yml'
distribution DistributeTestsBy.METHOD
}
allParallelSlowIntegrationTest {
testGroups 'slowIntegrationTest'
profile 'generalPurpose.yml'
distribution DistributeTestsBy.METHOD
}
}
task allParallelIntegrationTest(type: ParallelTestGroup) { ignoredTests = [
dependsOn dependxiesModule ':core-deterministic:testing:data:test'
podLogLevel PodLogLevel.INFO ]
testGroups "integrationTest"
numberOfShards generalPurpose.numberOfShards
streamOutput generalPurpose.streamOutput
coresPerFork generalPurpose.coresPerFork
memoryInGbPerFork generalPurpose.memoryInGbPerFork
nodeTaints generalPurpose.nodeTaints
distribute DistributeTestsBy.METHOD
} }
task allParallelUnitTest(type: ParallelTestGroup) {
dependsOn dependxiesModule
podLogLevel PodLogLevel.INFO
testGroups "test"
numberOfShards generalPurpose.numberOfShards
streamOutput generalPurpose.streamOutput
coresPerFork generalPurpose.coresPerFork
memoryInGbPerFork generalPurpose.memoryInGbPerFork
nodeTaints generalPurpose.nodeTaints
distribute DistributeTestsBy.CLASS
}
task allParallelUnitAndIntegrationTest(type: ParallelTestGroup) {
dependsOn dependxiesModule
testGroups "test", "integrationTest"
numberOfShards generalPurpose.numberOfShards
streamOutput generalPurpose.streamOutput
coresPerFork generalPurpose.coresPerFork
memoryInGbPerFork generalPurpose.memoryInGbPerFork
nodeTaints generalPurpose.nodeTaints
distribute DistributeTestsBy.METHOD
}
task parallelRegressionTest(type: ParallelTestGroup) {
testGroups "test", "integrationTest", "smokeTest"
dependsOn dependxiesModule
numberOfShards generalPurpose.numberOfShards
streamOutput generalPurpose.streamOutput
coresPerFork generalPurpose.coresPerFork
memoryInGbPerFork generalPurpose.memoryInGbPerFork
nodeTaints generalPurpose.nodeTaints
distribute DistributeTestsBy.METHOD
}
task allParallelSmokeTest(type: ParallelTestGroup) {
testGroups "smokeTest"
dependsOn dependxiesModule
numberOfShards generalPurpose.numberOfShards
streamOutput generalPurpose.streamOutput
coresPerFork generalPurpose.coresPerFork
memoryInGbPerFork generalPurpose.memoryInGbPerFork
nodeTaints generalPurpose.nodeTaints
distribute DistributeTestsBy.METHOD
}
task allParallelSlowIntegrationTest(type: ParallelTestGroup) {
testGroups "slowIntegrationTest"
dependsOn dependxiesModule
numberOfShards generalPurpose.numberOfShards
streamOutput generalPurpose.streamOutput
coresPerFork generalPurpose.coresPerFork
memoryInGbPerFork generalPurpose.memoryInGbPerFork
nodeTaints generalPurpose.nodeTaints
distribute DistributeTestsBy.METHOD
}
apply plugin: 'com.r3.testing.distributed-testing'
apply plugin: 'com.r3.testing.image-building'

View File

@ -10,7 +10,6 @@ import net.corda.client.rpc.ConnectionFailureException
import net.corda.client.rpc.CordaRPCClientConfiguration import net.corda.client.rpc.CordaRPCClientConfiguration
import net.corda.client.rpc.RPCException import net.corda.client.rpc.RPCException
import net.corda.client.rpc.RPCSinceVersion import net.corda.client.rpc.RPCSinceVersion
import net.corda.nodeapi.internal.rpc.client.RpcClientObservableDeSerializer
import net.corda.core.context.Actor import net.corda.core.context.Actor
import net.corda.core.context.Trace import net.corda.core.context.Trace
import net.corda.core.context.Trace.InvocationId import net.corda.core.context.Trace.InvocationId
@ -35,6 +34,7 @@ import net.corda.nodeapi.internal.DeduplicationChecker
import net.corda.nodeapi.internal.rpc.client.CallSite import net.corda.nodeapi.internal.rpc.client.CallSite
import net.corda.nodeapi.internal.rpc.client.CallSiteMap import net.corda.nodeapi.internal.rpc.client.CallSiteMap
import net.corda.nodeapi.internal.rpc.client.ObservableContext import net.corda.nodeapi.internal.rpc.client.ObservableContext
import net.corda.nodeapi.internal.rpc.client.RpcClientObservableDeSerializer
import net.corda.nodeapi.internal.rpc.client.RpcObservableMap import net.corda.nodeapi.internal.rpc.client.RpcObservableMap
import org.apache.activemq.artemis.api.core.ActiveMQException import org.apache.activemq.artemis.api.core.ActiveMQException
import org.apache.activemq.artemis.api.core.ActiveMQNotConnectedException import org.apache.activemq.artemis.api.core.ActiveMQNotConnectedException
@ -50,6 +50,7 @@ import org.apache.activemq.artemis.api.core.client.FailoverEventType
import org.apache.activemq.artemis.api.core.client.ServerLocator import org.apache.activemq.artemis.api.core.client.ServerLocator
import rx.Notification import rx.Notification
import rx.Observable import rx.Observable
import rx.exceptions.OnErrorNotImplementedException
import rx.subjects.UnicastSubject import rx.subjects.UnicastSubject
import java.lang.reflect.InvocationHandler import java.lang.reflect.InvocationHandler
import java.lang.reflect.Method import java.lang.reflect.Method
@ -142,6 +143,19 @@ internal class RPCClientProxyHandler(
} }
} }
} }
@Suppress("TooGenericExceptionCaught")
private fun closeObservable(observable: UnicastSubject<Notification<*>>) {
// Notify listeners of the observables that the connection is being terminated.
try {
observable.onError(ConnectionFailureException())
} catch (ex: OnErrorNotImplementedException) {
// Indicates the observer does not have any error handling.
log.debug { "Closed connection on observable whose observers have no error handling." }
} catch (ex: Exception) {
log.error("Unexpected exception when RPC connection failure handling", ex)
}
}
} }
// Used for reaping // Used for reaping
@ -452,14 +466,9 @@ internal class RPCClientProxyHandler(
} }
reaperScheduledFuture?.cancel(false) reaperScheduledFuture?.cancel(false)
val observablesMap = observableContext.observableMap.asMap() observableContext.observableMap.asMap().forEach { (key, observable) ->
observablesMap.keys.forEach { key ->
observationExecutorPool.run(key) { observationExecutorPool.run(key) {
try { observable?.also(Companion::closeObservable)
observablesMap[key]?.onError(ConnectionFailureException())
} catch (e: Exception) {
log.error("Unexpected exception when RPC connection failure handling", e)
}
} }
} }
observableContext.observableMap.invalidateAll() observableContext.observableMap.invalidateAll()

View File

@ -9,26 +9,32 @@ import org.junit.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
class ExceptionsErrorCodeFunctionsTest { class ExceptionsErrorCodeFunctionsTest {
private companion object {
@Test(timeout=3_000) private const val EXCEPTION_MESSAGE = "This is exception "
fun `error code for message prints out message and full stack trace`() { private const val TEST_MESSAGE = "This is a test message"
val originalMessage = SimpleMessage("This is a test message") private fun makeChain(previous: Exception?, ttl: Int): Exception {
var previous: Exception? = null val current = TestThrowable(ttl, previous)
val throwables = (0..10).map { return if (ttl == 0) {
val current = TestThrowable(it, previous)
previous = current
current current
} else {
makeChain(current, ttl - 1)
} }
val exception = throwables.last() }
}
@Test(timeout=5_000)
fun `error code for message prints out message and full stack trace`() {
val originalMessage = SimpleMessage(TEST_MESSAGE)
val exception = makeChain(null, 10)
val message = originalMessage.withErrorCodeFor(exception, Level.ERROR) val message = originalMessage.withErrorCodeFor(exception, Level.ERROR)
assertThat(message.formattedMessage, contains("This is a test message".toRegex())) assertThat(message.formattedMessage, contains(TEST_MESSAGE.toRegex()))
for (i in (0..10)) { for (i in (0..10)) {
assertThat(message.formattedMessage, contains("This is exception $i".toRegex())) assertThat(message.formattedMessage, contains("$EXCEPTION_MESSAGE $i".toRegex()))
} }
assertEquals(message.format, originalMessage.format) assertEquals(message.format, originalMessage.format)
assertEquals(message.parameters, originalMessage.parameters) assertEquals(message.parameters, originalMessage.parameters)
assertEquals(message.throwable, originalMessage.throwable) assertEquals(message.throwable, originalMessage.throwable)
} }
private class TestThrowable(index: Int, cause: Exception?) : Exception("This is exception $index", cause) private class TestThrowable(index: Int, cause: Exception?) : Exception("$EXCEPTION_MESSAGE $index", cause)
} }

View File

@ -14,8 +14,7 @@ java8MinUpdateVersion=171
platformVersion=8 platformVersion=8
guavaVersion=28.0-jre guavaVersion=28.0-jre
# Quasar version to use with Java 8: # Quasar version to use with Java 8:
quasarVersion=0.7.12_r3 quasarVersion=0.7.13_r3
quasarClassifier=jdk8
# Quasar version to use with Java 11: # Quasar version to use with Java 11:
quasarVersion11=0.8.0_r3 quasarVersion11=0.8.0_r3
jdkClassifier11=jdk11 jdkClassifier11=jdk11

View File

@ -25,10 +25,7 @@ tasks.named('jar', Jar) {
enabled = false enabled = false
} }
test { def test = tasks.named('test', Test) {
ext {
ignoreForDistribution = true
}
filter { filter {
// Running this class is the whole point, so include it explicitly. // Running this class is the whole point, so include it explicitly.
includeTestsMatching "net.corda.deterministic.data.GenerateData" includeTestsMatching "net.corda.deterministic.data.GenerateData"
@ -37,8 +34,9 @@ test {
// note: required by Gradle Build Cache. // note: required by Gradle Build Cache.
outputs.upToDateWhen { false } outputs.upToDateWhen { false }
} }
assemble.finalizedBy test
def testDataJar = file("$buildDir/test-data.jar")
artifacts { artifacts {
testData file: file("$buildDir/test-data.jar"), type: 'jar', builtBy: test archives file: testDataJar, type: 'jar', builtBy: test
testData file: testDataJar, type: 'jar', builtBy: test
} }

View File

@ -96,5 +96,5 @@ class FinalityFlowTests : WithFinality {
} }
/** "Old" CorDapp which will force its node to keep its FinalityHandler enabled */ /** "Old" CorDapp which will force its node to keep its FinalityHandler enabled */
private fun tokenOldCordapp() = cordappWithPackages("com.template").copy(targetPlatformVersion = 3) private fun tokenOldCordapp() = cordappWithPackages().copy(targetPlatformVersion = 3)
} }

View File

@ -56,7 +56,9 @@ class ReceiveFinalityFlowTest {
bob.assertFlowSentForObservationDueToUntrustedAttachmentsException(paymentReceiverId) bob.assertFlowSentForObservationDueToUntrustedAttachmentsException(paymentReceiverId)
// Restart Bob with the contracts CorDapp so that it can recover from the error // Restart Bob with the contracts CorDapp so that it can recover from the error
bob = mockNet.restartNode(bob, parameters = InternalMockNodeParameters(additionalCordapps = listOf(FINANCE_CONTRACTS_CORDAPP))) bob = mockNet.restartNode(bob,
parameters = InternalMockNodeParameters(additionalCordapps = listOf(FINANCE_CONTRACTS_CORDAPP)),
nodeFactory = { args -> InternalMockNetwork.MockNode(args, allowAppSchemaUpgradeWithCheckpoints = true) })
mockNet.runNetwork() mockNet.runNetwork()
assertThat(bob.services.getCashBalance(GBP)).isEqualTo(100.POUNDS) assertThat(bob.services.getCashBalance(GBP)).isEqualTo(100.POUNDS)
} }

View File

@ -618,7 +618,6 @@
<ID>LongParameterList:ArtemisRpcBroker.kt$ArtemisRpcBroker.Companion$(configuration: MutualSslConfiguration, address: NetworkHostAndPort, adminAddress: NetworkHostAndPort, securityManager: RPCSecurityManager, maxMessageSize: Int, journalBufferTimeout: Int?, jmxEnabled: Boolean, baseDirectory: Path, shouldStartLocalShell: Boolean)</ID> <ID>LongParameterList:ArtemisRpcBroker.kt$ArtemisRpcBroker.Companion$(configuration: MutualSslConfiguration, address: NetworkHostAndPort, adminAddress: NetworkHostAndPort, securityManager: RPCSecurityManager, maxMessageSize: Int, journalBufferTimeout: Int?, jmxEnabled: Boolean, baseDirectory: Path, shouldStartLocalShell: Boolean)</ID>
<ID>LongParameterList:ArtemisRpcBroker.kt$ArtemisRpcBroker.Companion$(configuration: MutualSslConfiguration, address: NetworkHostAndPort, adminAddress: NetworkHostAndPort, sslOptions: BrokerRpcSslOptions, securityManager: RPCSecurityManager, maxMessageSize: Int, journalBufferTimeout: Int?, jmxEnabled: Boolean, baseDirectory: Path, shouldStartLocalShell: Boolean)</ID> <ID>LongParameterList:ArtemisRpcBroker.kt$ArtemisRpcBroker.Companion$(configuration: MutualSslConfiguration, address: NetworkHostAndPort, adminAddress: NetworkHostAndPort, sslOptions: BrokerRpcSslOptions, securityManager: RPCSecurityManager, maxMessageSize: Int, journalBufferTimeout: Int?, jmxEnabled: Boolean, baseDirectory: Path, shouldStartLocalShell: Boolean)</ID>
<ID>LongParameterList:ArtemisRpcTests.kt$ArtemisRpcTests$(nodeSSlconfig: MutualSslConfiguration, brokerSslOptions: BrokerRpcSslOptions?, useSslForBroker: Boolean, clientSslOptions: ClientRpcSslOptions?, address: NetworkHostAndPort = ports.nextHostAndPort(), adminAddress: NetworkHostAndPort = ports.nextHostAndPort(), baseDirectory: Path = tempFolder.root.toPath() )</ID> <ID>LongParameterList:ArtemisRpcTests.kt$ArtemisRpcTests$(nodeSSlconfig: MutualSslConfiguration, brokerSslOptions: BrokerRpcSslOptions?, useSslForBroker: Boolean, clientSslOptions: ClientRpcSslOptions?, address: NetworkHostAndPort = ports.nextHostAndPort(), adminAddress: NetworkHostAndPort = ports.nextHostAndPort(), baseDirectory: Path = tempFolder.root.toPath() )</ID>
<ID>LongParameterList:AttachmentsClassLoader.kt$AttachmentsClassLoaderBuilder$(attachments: List&lt;Attachment&gt;, params: NetworkParameters, txId: SecureHash, isAttachmentTrusted: (Attachment) -&gt; Boolean, parent: ClassLoader = ClassLoader.getSystemClassLoader(), block: (ClassLoader) -&gt; T)</ID>
<ID>LongParameterList:BFTSmart.kt$BFTSmart.Replica$( states: List&lt;StateRef&gt;, txId: SecureHash, callerName: CordaX500Name, requestSignature: NotarisationRequestSignature, timeWindow: TimeWindow?, references: List&lt;StateRef&gt; = emptyList() )</ID> <ID>LongParameterList:BFTSmart.kt$BFTSmart.Replica$( states: List&lt;StateRef&gt;, txId: SecureHash, callerName: CordaX500Name, requestSignature: NotarisationRequestSignature, timeWindow: TimeWindow?, references: List&lt;StateRef&gt; = emptyList() )</ID>
<ID>LongParameterList:BusinessCalendar.kt$BusinessCalendar.Companion$(startDate: LocalDate, period: Frequency, calendar: BusinessCalendar = EMPTY, dateRollConvention: DateRollConvention = DateRollConvention.Following, noOfAdditionalPeriods: Int = Integer.MAX_VALUE, endDate: LocalDate? = null, periodOffset: Int? = null)</ID> <ID>LongParameterList:BusinessCalendar.kt$BusinessCalendar.Companion$(startDate: LocalDate, period: Frequency, calendar: BusinessCalendar = EMPTY, dateRollConvention: DateRollConvention = DateRollConvention.Following, noOfAdditionalPeriods: Int = Integer.MAX_VALUE, endDate: LocalDate? = null, periodOffset: Int? = null)</ID>
<ID>LongParameterList:Cash.kt$Cash$(inputs: List&lt;State&gt;, outputs: List&lt;State&gt;, tx: LedgerTransaction, issueCommand: CommandWithParties&lt;Commands.Issue&gt;, currency: Currency, issuer: PartyAndReference)</ID> <ID>LongParameterList:Cash.kt$Cash$(inputs: List&lt;State&gt;, outputs: List&lt;State&gt;, tx: LedgerTransaction, issueCommand: CommandWithParties&lt;Commands.Issue&gt;, currency: Currency, issuer: PartyAndReference)</ID>
@ -719,7 +718,6 @@
<ID>LongParameterList:X509Utilities.kt$X509Utilities$(certificateType: CertificateType, issuer: X500Principal, issuerPublicKey: PublicKey, issuerSigner: ContentSigner, subject: X500Principal, subjectPublicKey: PublicKey, validityWindow: Pair&lt;Date, Date&gt;, nameConstraints: NameConstraints? = null, crlDistPoint: String? = null, crlIssuer: X500Name? = null)</ID> <ID>LongParameterList:X509Utilities.kt$X509Utilities$(certificateType: CertificateType, issuer: X500Principal, issuerPublicKey: PublicKey, issuerSigner: ContentSigner, subject: X500Principal, subjectPublicKey: PublicKey, validityWindow: Pair&lt;Date, Date&gt;, nameConstraints: NameConstraints? = null, crlDistPoint: String? = null, crlIssuer: X500Name? = null)</ID>
<ID>LongParameterList:X509Utilities.kt$X509Utilities$(certificateType: CertificateType, issuer: X500Principal, issuerPublicKey: PublicKey, subject: X500Principal, subjectPublicKey: PublicKey, validityWindow: Pair&lt;Date, Date&gt;, nameConstraints: NameConstraints? = null, crlDistPoint: String? = null, crlIssuer: X500Name? = null)</ID> <ID>LongParameterList:X509Utilities.kt$X509Utilities$(certificateType: CertificateType, issuer: X500Principal, issuerPublicKey: PublicKey, subject: X500Principal, subjectPublicKey: PublicKey, validityWindow: Pair&lt;Date, Date&gt;, nameConstraints: NameConstraints? = null, crlDistPoint: String? = null, crlIssuer: X500Name? = null)</ID>
<ID>LongParameterList:X509Utilities.kt$X509Utilities$(certificateType: CertificateType, issuerCertificate: X509Certificate, issuerKeyPair: KeyPair, subject: X500Principal, subjectPublicKey: PublicKey, validityWindow: Pair&lt;Duration, Duration&gt; = DEFAULT_VALIDITY_WINDOW, nameConstraints: NameConstraints? = null, crlDistPoint: String? = null, crlIssuer: X500Name? = null)</ID> <ID>LongParameterList:X509Utilities.kt$X509Utilities$(certificateType: CertificateType, issuerCertificate: X509Certificate, issuerKeyPair: KeyPair, subject: X500Principal, subjectPublicKey: PublicKey, validityWindow: Pair&lt;Duration, Duration&gt; = DEFAULT_VALIDITY_WINDOW, nameConstraints: NameConstraints? = null, crlDistPoint: String? = null, crlIssuer: X500Name? = null)</ID>
<ID>LongParameterList:internalAccessTestHelpers.kt$( inputs: List&lt;StateAndRef&lt;ContractState&gt;&gt;, outputs: List&lt;TransactionState&lt;ContractState&gt;&gt;, commands: List&lt;CommandWithParties&lt;CommandData&gt;&gt;, attachments: List&lt;Attachment&gt;, id: SecureHash, notary: Party?, timeWindow: TimeWindow?, privacySalt: PrivacySalt, networkParameters: NetworkParameters, references: List&lt;StateAndRef&lt;ContractState&gt;&gt;, componentGroups: List&lt;ComponentGroup&gt;? = null, serializedInputs: List&lt;SerializedStateAndRef&gt;? = null, serializedReferences: List&lt;SerializedStateAndRef&gt;? = null, isAttachmentTrusted: (Attachment) -&gt; Boolean )</ID>
<ID>MagicNumber:AMQPClientSerializationScheme.kt$AMQPClientSerializationScheme.Companion$128</ID> <ID>MagicNumber:AMQPClientSerializationScheme.kt$AMQPClientSerializationScheme.Companion$128</ID>
<ID>MagicNumber:AMQPSerializationScheme.kt$AbstractAMQPSerializationScheme$128</ID> <ID>MagicNumber:AMQPSerializationScheme.kt$AbstractAMQPSerializationScheme$128</ID>
<ID>MagicNumber:AMQPServer.kt$AMQPServer$100</ID> <ID>MagicNumber:AMQPServer.kt$AMQPServer$100</ID>
@ -1284,7 +1282,6 @@
<ID>SpreadOperator:FlowFrameworkTripartyTests.kt$FlowFrameworkTripartyTests$(*expected)</ID> <ID>SpreadOperator:FlowFrameworkTripartyTests.kt$FlowFrameworkTripartyTests$(*expected)</ID>
<ID>SpreadOperator:FlowLogicRefFactoryImpl.kt$FlowLogicRefFactoryImpl$(flowClass, *args)</ID> <ID>SpreadOperator:FlowLogicRefFactoryImpl.kt$FlowLogicRefFactoryImpl$(flowClass, *args)</ID>
<ID>SpreadOperator:FlowOverrideTests.kt$FlowOverrideTests$(*nodeAClasses.toTypedArray())</ID> <ID>SpreadOperator:FlowOverrideTests.kt$FlowOverrideTests$(*nodeAClasses.toTypedArray())</ID>
<ID>SpreadOperator:FlowOverrideTests.kt$FlowOverrideTests$(*nodeBClasses.toTypedArray())</ID>
<ID>SpreadOperator:FlowTestsUtils.kt$(*allSessions)</ID> <ID>SpreadOperator:FlowTestsUtils.kt$(*allSessions)</ID>
<ID>SpreadOperator:FlowTestsUtils.kt$(session, *sessions)</ID> <ID>SpreadOperator:FlowTestsUtils.kt$(session, *sessions)</ID>
<ID>SpreadOperator:HTTPNetworkRegistrationService.kt$HTTPNetworkRegistrationService$(OpaqueBytes(request.encoded), "Platform-Version" to "${versionInfo.platformVersion}", "Client-Version" to versionInfo.releaseVersion, "Private-Network-Map" to (config.pnm?.toString() ?: ""), *(config.csrToken?.let { arrayOf(CENM_SUBMISSION_TOKEN to it) } ?: arrayOf()))</ID> <ID>SpreadOperator:HTTPNetworkRegistrationService.kt$HTTPNetworkRegistrationService$(OpaqueBytes(request.encoded), "Platform-Version" to "${versionInfo.platformVersion}", "Client-Version" to versionInfo.releaseVersion, "Private-Network-Map" to (config.pnm?.toString() ?: ""), *(config.csrToken?.let { arrayOf(CENM_SUBMISSION_TOKEN to it) } ?: arrayOf()))</ID>
@ -1416,7 +1413,6 @@
<ID>ThrowsCount:StructuresTests.kt$AttachmentTest$@Test(timeout=300_000) fun `openAsJAR does not leak file handle if attachment has corrupted manifest`()</ID> <ID>ThrowsCount:StructuresTests.kt$AttachmentTest$@Test(timeout=300_000) fun `openAsJAR does not leak file handle if attachment has corrupted manifest`()</ID>
<ID>ThrowsCount:TransactionVerifierServiceInternal.kt$Verifier$ private fun getUniqueContractAttachmentsByContract(): Map&lt;ContractClassName, ContractAttachment&gt;</ID> <ID>ThrowsCount:TransactionVerifierServiceInternal.kt$Verifier$ private fun getUniqueContractAttachmentsByContract(): Map&lt;ContractClassName, ContractAttachment&gt;</ID>
<ID>ThrowsCount:TransactionVerifierServiceInternal.kt$Verifier$// Using basic graph theory, a full cycle of encumbered (co-dependent) states should exist to achieve bi-directional // encumbrances. This property is important to ensure that no states involved in an encumbrance-relationship // can be spent on their own. Briefly, if any of the states is having more than one encumbrance references by // other states, a full cycle detection will fail. As a result, all of the encumbered states must be present // as "from" and "to" only once (or zero times if no encumbrance takes place). For instance, // a -&gt; b // c -&gt; b and a -&gt; b // b -&gt; a b -&gt; c // do not satisfy the bi-directionality (full cycle) property. // // In the first example "b" appears twice in encumbrance ("to") list and "c" exists in the encumbered ("from") list only. // Due the above, one could consume "a" and "b" in the same transaction and then, because "b" is already consumed, "c" cannot be spent. // // Similarly, the second example does not form a full cycle because "a" and "c" exist in one of the lists only. // As a result, one can consume "b" and "c" in the same transactions, which will make "a" impossible to be spent. // // On other hand the following are valid constructions: // a -&gt; b a -&gt; c // b -&gt; c and c -&gt; b // c -&gt; a b -&gt; a // and form a full cycle, meaning that the bi-directionality property is satisfied. private fun checkBidirectionalOutputEncumbrances(statesAndEncumbrance: List&lt;Pair&lt;Int, Int&gt;&gt;)</ID> <ID>ThrowsCount:TransactionVerifierServiceInternal.kt$Verifier$// Using basic graph theory, a full cycle of encumbered (co-dependent) states should exist to achieve bi-directional // encumbrances. This property is important to ensure that no states involved in an encumbrance-relationship // can be spent on their own. Briefly, if any of the states is having more than one encumbrance references by // other states, a full cycle detection will fail. As a result, all of the encumbered states must be present // as "from" and "to" only once (or zero times if no encumbrance takes place). For instance, // a -&gt; b // c -&gt; b and a -&gt; b // b -&gt; a b -&gt; c // do not satisfy the bi-directionality (full cycle) property. // // In the first example "b" appears twice in encumbrance ("to") list and "c" exists in the encumbered ("from") list only. // Due the above, one could consume "a" and "b" in the same transaction and then, because "b" is already consumed, "c" cannot be spent. // // Similarly, the second example does not form a full cycle because "a" and "c" exist in one of the lists only. // As a result, one can consume "b" and "c" in the same transactions, which will make "a" impossible to be spent. // // On other hand the following are valid constructions: // a -&gt; b a -&gt; c // b -&gt; c and c -&gt; b // c -&gt; a b -&gt; a // and form a full cycle, meaning that the bi-directionality property is satisfied. private fun checkBidirectionalOutputEncumbrances(statesAndEncumbrance: List&lt;Pair&lt;Int, Int&gt;&gt;)</ID>
<ID>ThrowsCount:WireTransaction.kt$WireTransaction$private fun toLedgerTransactionInternal( resolveIdentity: (PublicKey) -&gt; Party?, resolveAttachment: (SecureHash) -&gt; Attachment?, resolveStateRefAsSerialized: (StateRef) -&gt; SerializedBytes&lt;TransactionState&lt;ContractState&gt;&gt;?, resolveParameters: (SecureHash?) -&gt; NetworkParameters?, isAttachmentTrusted: (Attachment) -&gt; Boolean ): LedgerTransaction</ID>
<ID>ThrowsCount:WireTransaction.kt$WireTransaction.Companion$ @CordaInternal fun resolveStateRefBinaryComponent(stateRef: StateRef, services: ServicesForResolution): SerializedBytes&lt;TransactionState&lt;ContractState&gt;&gt;?</ID> <ID>ThrowsCount:WireTransaction.kt$WireTransaction.Companion$ @CordaInternal fun resolveStateRefBinaryComponent(stateRef: StateRef, services: ServicesForResolution): SerializedBytes&lt;TransactionState&lt;ContractState&gt;&gt;?</ID>
<ID>TooGenericExceptionCaught:AMQPChannelHandler.kt$AMQPChannelHandler$ex: Exception</ID> <ID>TooGenericExceptionCaught:AMQPChannelHandler.kt$AMQPChannelHandler$ex: Exception</ID>
<ID>TooGenericExceptionCaught:AMQPExceptions.kt$th: Throwable</ID> <ID>TooGenericExceptionCaught:AMQPExceptions.kt$th: Throwable</ID>
@ -1465,7 +1461,6 @@
<ID>TooGenericExceptionCaught:DriverDSLImpl.kt$DriverDSLImpl.Companion$th: Throwable</ID> <ID>TooGenericExceptionCaught:DriverDSLImpl.kt$DriverDSLImpl.Companion$th: Throwable</ID>
<ID>TooGenericExceptionCaught:DriverDSLImpl.kt$exception: Throwable</ID> <ID>TooGenericExceptionCaught:DriverDSLImpl.kt$exception: Throwable</ID>
<ID>TooGenericExceptionCaught:DriverTests.kt$DriverTests$e: Exception</ID> <ID>TooGenericExceptionCaught:DriverTests.kt$DriverTests$e: Exception</ID>
<ID>TooGenericExceptionCaught:ErrorCodeLoggingTests.kt$e: Exception</ID>
<ID>TooGenericExceptionCaught:ErrorHandling.kt$ErrorHandling.CheckpointAfterErrorFlow$t: Throwable</ID> <ID>TooGenericExceptionCaught:ErrorHandling.kt$ErrorHandling.CheckpointAfterErrorFlow$t: Throwable</ID>
<ID>TooGenericExceptionCaught:EventProcessor.kt$EventProcessor$ex: Exception</ID> <ID>TooGenericExceptionCaught:EventProcessor.kt$EventProcessor$ex: Exception</ID>
<ID>TooGenericExceptionCaught:Eventually.kt$e: Exception</ID> <ID>TooGenericExceptionCaught:Eventually.kt$e: Exception</ID>
@ -1677,6 +1672,7 @@
<ID>TooManyFunctions:RPCApi.kt$net.corda.nodeapi.RPCApi.kt</ID> <ID>TooManyFunctions:RPCApi.kt$net.corda.nodeapi.RPCApi.kt</ID>
<ID>TooManyFunctions:RPCClientProxyHandler.kt$RPCClientProxyHandler : InvocationHandler</ID> <ID>TooManyFunctions:RPCClientProxyHandler.kt$RPCClientProxyHandler : InvocationHandler</ID>
<ID>TooManyFunctions:RPCServer.kt$RPCServer</ID> <ID>TooManyFunctions:RPCServer.kt$RPCServer</ID>
<ID>TooManyFunctions:SSLHelper.kt$net.corda.nodeapi.internal.protonwrapper.netty.SSLHelper.kt</ID>
<ID>TooManyFunctions:SerializationHelper.kt$net.corda.serialization.internal.amqp.SerializationHelper.kt</ID> <ID>TooManyFunctions:SerializationHelper.kt$net.corda.serialization.internal.amqp.SerializationHelper.kt</ID>
<ID>TooManyFunctions:ServiceHub.kt$ServiceHub : ServicesForResolution</ID> <ID>TooManyFunctions:ServiceHub.kt$ServiceHub : ServicesForResolution</ID>
<ID>TooManyFunctions:SignedTransaction.kt$SignedTransaction : TransactionWithSignatures</ID> <ID>TooManyFunctions:SignedTransaction.kt$SignedTransaction : TransactionWithSignatures</ID>

Binary file not shown.

View File

@ -15,7 +15,6 @@ import net.corda.core.crypto.Crypto.generateKeyPair
import net.corda.core.crypto.SignatureScheme import net.corda.core.crypto.SignatureScheme
import net.corda.core.crypto.newSecureRandom import net.corda.core.crypto.newSecureRandom
import net.corda.core.identity.CordaX500Name import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.JavaVersion
import net.corda.core.internal.div import net.corda.core.internal.div
import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializationContext
import net.corda.core.serialization.deserialize import net.corda.core.serialization.deserialize
@ -118,7 +117,7 @@ class X509UtilitiesTest {
@Test(timeout=300_000) @Test(timeout=300_000)
fun `create valid self-signed CA certificate`() { fun `create valid self-signed CA certificate`() {
Crypto.supportedSignatureSchemes().filter { it != COMPOSITE_KEY Crypto.supportedSignatureSchemes().filter { it != COMPOSITE_KEY
&& ( !JavaVersion.isVersionAtLeast(JavaVersion.Java_11) || it != SPHINCS256_SHA256)}.forEach { validSelfSignedCertificate(it) } && ( it != SPHINCS256_SHA256)}.forEach { validSelfSignedCertificate(it) }
} }
private fun validSelfSignedCertificate(signatureScheme: SignatureScheme) { private fun validSelfSignedCertificate(signatureScheme: SignatureScheme) {
@ -153,7 +152,7 @@ class X509UtilitiesTest {
@Test(timeout=300_000) @Test(timeout=300_000)
fun `create valid server certificate chain`() { fun `create valid server certificate chain`() {
certChainSchemeCombinations.filter{ !JavaVersion.isVersionAtLeast(JavaVersion.Java_11) || it.first != SPHINCS256_SHA256 } certChainSchemeCombinations.filter{ it.first != SPHINCS256_SHA256 }
.forEach { createValidServerCertChain(it.first, it.second) } .forEach { createValidServerCertChain(it.first, it.second) }
} }

View File

@ -0,0 +1,108 @@
package net.corda.nodeapitests.internal.persistence
import net.corda.core.contracts.UniqueIdentifier
import net.corda.core.schemas.MappedSchema
import net.corda.core.schemas.PersistentState
import net.corda.core.schemas.PersistentStateRef
import net.corda.node.internal.DataSourceFactory
import net.corda.node.internal.startHikariPool
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseMigrationException
import net.corda.nodeapi.internal.persistence.HibernateSchemaChangeException
import net.corda.nodeapi.internal.persistence.SchemaMigration
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.TestIdentity
import net.corda.testing.internal.TestingNamedCacheFactory
import net.corda.testing.node.MockServices
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Before
import org.junit.Test
import java.util.*
import javax.persistence.Column
import javax.persistence.Entity
import javax.persistence.Table
import javax.sql.DataSource
class MigrationSchemaSyncTest{
object TestSchemaFamily
object GoodSchema : MappedSchema(schemaFamily = TestSchemaFamily.javaClass, version = 1, mappedTypes = listOf(State::class.java)) {
@Entity
@Table(name = "State")
class State(
@Column
var id: String
) : PersistentState(PersistentStateRef(UniqueIdentifier().toString(), 0 ))
override val migrationResource: String? = "goodschema.testmigration"
}
lateinit var hikariProperties: Properties
lateinit var dataSource: DataSource
@Before
fun setUp() {
hikariProperties = MockServices.makeTestDataSourceProperties()
dataSource = DataSourceFactory.createDataSource(hikariProperties)
}
private fun schemaMigration() = SchemaMigration(dataSource, null, null,
TestIdentity(ALICE_NAME, 70).name)
@Test(timeout=300_000)
fun testSchemaScript(){
schemaMigration().runMigration(false, setOf(GoodSchema), true)
val persistence = CordaPersistence(
false,
setOf(GoodSchema),
hikariProperties.getProperty("dataSource.url"),
TestingNamedCacheFactory()
)
persistence.startHikariPool(hikariProperties){ _, _ -> Unit}
persistence.transaction {
this.entityManager.persist(GoodSchema.State("id"))
}
}
@Test(timeout=300_000)
fun checkThatSchemaSyncFixesLiquibaseException(){
// Schema is missing if no migration is run and hibernate not allowed to create
val persistenceBlank = CordaPersistence(
false,
setOf(GoodSchema),
hikariProperties.getProperty("dataSource.url"),
TestingNamedCacheFactory()
)
persistenceBlank.startHikariPool(hikariProperties){ _, _ -> Unit}
assertThatThrownBy{ persistenceBlank.transaction {this.entityManager.persist(GoodSchema.State("id"))}}
.isInstanceOf(HibernateSchemaChangeException::class.java)
.hasMessageContaining("Incompatible schema")
// create schema via hibernate - now schema gets created and we can write
val persistenceHibernate = CordaPersistence(
false,
setOf(GoodSchema),
hikariProperties.getProperty("dataSource.url"),
TestingNamedCacheFactory(),
allowHibernateToManageAppSchema = true
)
persistenceHibernate.startHikariPool(hikariProperties){ _, _ -> Unit}
persistenceHibernate.transaction { entityManager.persist(GoodSchema.State("id_hibernate")) }
// if we try to run schema migration now, the changelog and the schemas are out of sync
assertThatThrownBy { schemaMigration().runMigration(false, setOf(GoodSchema), true) }
.isInstanceOf(DatabaseMigrationException::class.java)
.hasMessageContaining("Table \"STATE\" already exists")
// update the change log with schemas we know exist
schemaMigration().synchroniseSchemas(setOf(GoodSchema), true)
// now run migration runs clean
schemaMigration().runMigration(false, setOf(GoodSchema), true)
}
}

View File

@ -2,12 +2,11 @@ package net.corda.nodeapitests.internal.persistence
import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.MappedSchema
import net.corda.core.schemas.PersistentState import net.corda.core.schemas.PersistentState
import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.nodeapi.internal.persistence.MissingMigrationException
import net.corda.nodeapi.internal.persistence.SchemaMigration
import net.corda.node.internal.DataSourceFactory import net.corda.node.internal.DataSourceFactory
import net.corda.node.services.persistence.DBCheckpointStorage import net.corda.node.services.persistence.DBCheckpointStorage
import net.corda.node.services.schema.NodeSchemaService import net.corda.node.services.schema.NodeSchemaService
import net.corda.nodeapi.internal.persistence.MissingMigrationException
import net.corda.nodeapi.internal.persistence.SchemaMigration
import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.TestIdentity import net.corda.testing.core.TestIdentity
import net.corda.testing.node.MockServices import net.corda.testing.node.MockServices
@ -40,25 +39,21 @@ class MissingSchemaMigrationTest {
dataSource = DataSourceFactory.createDataSource(hikariProperties) dataSource = DataSourceFactory.createDataSource(hikariProperties)
} }
private fun createSchemaMigration(schemasToMigrate: Set<MappedSchema>, forceThrowOnMissingMigration: Boolean): SchemaMigration { private fun schemaMigration() = SchemaMigration(dataSource, null, null,
val databaseConfig = DatabaseConfig() TestIdentity(ALICE_NAME, 70).name)
return SchemaMigration(schemasToMigrate, dataSource, databaseConfig, null, null,
TestIdentity(ALICE_NAME, 70).name, forceThrowOnMissingMigration)
}
@Test(timeout=300_000) @Test(timeout=300_000)
fun `test that an error is thrown when forceThrowOnMissingMigration is set and a mapped schema is missing a migration`() { fun `test that an error is thrown when forceThrowOnMissingMigration is set and a mapped schema is missing a migration`() {
assertThatThrownBy { assertThatThrownBy {
createSchemaMigration(setOf(GoodSchema), true) schemaMigration().runMigration(dataSource.connection.use { DBCheckpointStorage.getCheckpointCount(it) != 0L }, setOf(GoodSchema), true)
.nodeStartup(dataSource.connection.use { DBCheckpointStorage.getCheckpointCount(it) != 0L })
}.isInstanceOf(MissingMigrationException::class.java) }.isInstanceOf(MissingMigrationException::class.java)
} }
@Test(timeout=300_000) @Test(timeout=300_000)
fun `test that an error is not thrown when forceThrowOnMissingMigration is not set and a mapped schema is missing a migration`() { fun `test that an error is not thrown when forceThrowOnMissingMigration is not set and a mapped schema is missing a migration`() {
assertDoesNotThrow { assertDoesNotThrow {
createSchemaMigration(setOf(GoodSchema), false) schemaMigration().runMigration(dataSource.connection.use { DBCheckpointStorage.getCheckpointCount(it) != 0L }, setOf(GoodSchema), false)
.nodeStartup(dataSource.connection.use { DBCheckpointStorage.getCheckpointCount(it) != 0L })
} }
} }
@ -66,8 +61,7 @@ class MissingSchemaMigrationTest {
fun `test that there are no missing migrations for the node`() { fun `test that there are no missing migrations for the node`() {
assertDoesNotThrow("This test failure indicates " + assertDoesNotThrow("This test failure indicates " +
"a new table has been added to the node without the appropriate migration scripts being present") { "a new table has been added to the node without the appropriate migration scripts being present") {
createSchemaMigration(NodeSchemaService().internalSchemas(), false) schemaMigration().runMigration(dataSource.connection.use { DBCheckpointStorage.getCheckpointCount(it) != 0L }, NodeSchemaService().internalSchemas, true)
.nodeStartup(dataSource.connection.use { DBCheckpointStorage.getCheckpointCount(it) != 0L })
} }
} }

View File

@ -0,0 +1,19 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd"
logicalFilePath="migration/node-services.changelog-init.xml">
<changeSet author="R3.Corda" id="unittest-goodschema-v1">
<createTable tableName="State">
<column name="output_index" type="INT">
<constraints nullable="false"/>
</column>
<column name="transaction_id" type="NVARCHAR(64)">
<constraints nullable="false"/>
</column>
<column name="id" type="NVARCHAR(255)"/>
</createTable>
</changeSet>
</databaseChangeLog>

View File

@ -75,6 +75,15 @@ constructor(private val initSerEnv: Boolean,
"generate-node-info" "generate-node-info"
) )
private val createSchemasCmd = listOf(
Paths.get(System.getProperty("java.home"), "bin", "java").toString(),
"-jar",
"corda.jar",
"run-migration-scripts",
"--core-schemas",
"--app-schemas"
)
private const val LOGS_DIR_NAME = "logs" private const val LOGS_DIR_NAME = "logs"
private val jarsThatArentCordapps = setOf("corda.jar", "runnodes.jar") private val jarsThatArentCordapps = setOf("corda.jar", "runnodes.jar")
@ -92,7 +101,9 @@ constructor(private val initSerEnv: Boolean,
} }
val executor = Executors.newFixedThreadPool(numParallelProcesses) val executor = Executors.newFixedThreadPool(numParallelProcesses)
return try { return try {
nodeDirs.map { executor.fork { generateNodeInfo(it) } }.transpose().getOrThrow() nodeDirs.map { executor.fork {
createDbSchemas(it)
generateNodeInfo(it) } }.transpose().getOrThrow()
} finally { } finally {
warningTimer.cancel() warningTimer.cancel()
executor.shutdownNow() executor.shutdownNow()
@ -100,23 +111,31 @@ constructor(private val initSerEnv: Boolean,
} }
private fun generateNodeInfo(nodeDir: Path): Path { private fun generateNodeInfo(nodeDir: Path): Path {
runNodeJob(nodeInfoGenCmd, nodeDir, "node-info-gen.log")
return nodeDir.list { paths ->
paths.filter { it.fileName.toString().startsWith(NODE_INFO_FILE_NAME_PREFIX) }.findFirst().get()
}
}
private fun createDbSchemas(nodeDir: Path) {
runNodeJob(createSchemasCmd, nodeDir, "node-run-migration.log")
}
private fun runNodeJob(command: List<String>, nodeDir: Path, logfileName: String) {
val logsDir = (nodeDir / LOGS_DIR_NAME).createDirectories() val logsDir = (nodeDir / LOGS_DIR_NAME).createDirectories()
val nodeInfoGenFile = (logsDir / "node-info-gen.log").toFile() val nodeRedirectFile = (logsDir / logfileName).toFile()
val process = ProcessBuilder(nodeInfoGenCmd) val process = ProcessBuilder(command)
.directory(nodeDir.toFile()) .directory(nodeDir.toFile())
.redirectErrorStream(true) .redirectErrorStream(true)
.redirectOutput(nodeInfoGenFile) .redirectOutput(nodeRedirectFile)
.apply { environment()["CAPSULE_CACHE_DIR"] = "../.cache" } .apply { environment()["CAPSULE_CACHE_DIR"] = "../.cache" }
.start() .start()
try { try {
if (!process.waitFor(3, TimeUnit.MINUTES)) { if (!process.waitFor(3, TimeUnit.MINUTES)) {
process.destroyForcibly() process.destroyForcibly()
printNodeInfoGenLogToConsole(nodeInfoGenFile) printNodeOutputToConsoleAndThrow(nodeRedirectFile)
}
printNodeInfoGenLogToConsole(nodeInfoGenFile) { process.exitValue() == 0 }
return nodeDir.list { paths ->
paths.filter { it.fileName.toString().startsWith(NODE_INFO_FILE_NAME_PREFIX) }.findFirst().get()
} }
if (process.exitValue() != 0) printNodeOutputToConsoleAndThrow(nodeRedirectFile)
} catch (e: InterruptedException) { } catch (e: InterruptedException) {
// Don't leave this process dangling if the thread is interrupted. // Don't leave this process dangling if the thread is interrupted.
process.destroyForcibly() process.destroyForcibly()
@ -124,19 +143,17 @@ constructor(private val initSerEnv: Boolean,
} }
} }
private fun printNodeInfoGenLogToConsole(nodeInfoGenFile: File, check: (() -> Boolean) = { true }) { private fun printNodeOutputToConsoleAndThrow(stdoutFile: File) {
if (!check.invoke()) { val nodeDir = stdoutFile.parent
val nodeDir = nodeInfoGenFile.parent
val nodeIdentifier = try { val nodeIdentifier = try {
ConfigFactory.parseFile((nodeDir / "node.conf").toFile()).getString("myLegalName") ConfigFactory.parseFile((nodeDir / "node.conf").toFile()).getString("myLegalName")
} catch (e: ConfigException) { } catch (e: ConfigException) {
nodeDir nodeDir
} }
System.err.println("#### Error while generating node info file $nodeIdentifier ####") System.err.println("#### Error while generating node info file $nodeIdentifier ####")
nodeInfoGenFile.inputStream().copyTo(System.err) stdoutFile.inputStream().copyTo(System.err)
throw IllegalStateException("Error while generating node info file. Please check the logs in $nodeDir.") throw IllegalStateException("Error while generating node info file. Please check the logs in $nodeDir.")
} }
}
const val DEFAULT_MAX_MESSAGE_SIZE: Int = 10485760 const val DEFAULT_MAX_MESSAGE_SIZE: Int = 10485760
const val DEFAULT_MAX_TRANSACTION_SIZE: Int = 524288000 const val DEFAULT_MAX_TRANSACTION_SIZE: Int = 524288000

View File

@ -31,24 +31,12 @@ import javax.sql.DataSource
*/ */
const val NODE_DATABASE_PREFIX = "node_" const val NODE_DATABASE_PREFIX = "node_"
enum class SchemaInitializationType{
NONE,
VALIDATE,
UPDATE
}
// This class forms part of the node config and so any changes to it must be handled with care // This class forms part of the node config and so any changes to it must be handled with care
data class DatabaseConfig( data class DatabaseConfig(
val initialiseSchema: Boolean = Defaults.initialiseSchema,
val initialiseAppSchema: SchemaInitializationType = Defaults.initialiseAppSchema,
val transactionIsolationLevel: TransactionIsolationLevel = Defaults.transactionIsolationLevel,
val exportHibernateJMXStatistics: Boolean = Defaults.exportHibernateJMXStatistics, val exportHibernateJMXStatistics: Boolean = Defaults.exportHibernateJMXStatistics,
val mappedSchemaCacheSize: Long = Defaults.mappedSchemaCacheSize val mappedSchemaCacheSize: Long = Defaults.mappedSchemaCacheSize
) { ) {
object Defaults { object Defaults {
val initialiseSchema = true
val initialiseAppSchema = SchemaInitializationType.UPDATE
val transactionIsolationLevel = TransactionIsolationLevel.REPEATABLE_READ
val exportHibernateJMXStatistics = false val exportHibernateJMXStatistics = false
val mappedSchemaCacheSize = 100L val mappedSchemaCacheSize = 100L
} }
@ -67,6 +55,10 @@ enum class TransactionIsolationLevel {
*/ */
val jdbcString = "TRANSACTION_$name" val jdbcString = "TRANSACTION_$name"
val jdbcValue: Int = java.sql.Connection::class.java.getField(jdbcString).get(null) as Int val jdbcValue: Int = java.sql.Connection::class.java.getField(jdbcString).get(null) as Int
companion object{
val default = READ_COMMITTED
}
} }
internal val _prohibitDatabaseAccess = ThreadLocal.withInitial { false } internal val _prohibitDatabaseAccess = ThreadLocal.withInitial { false }
@ -96,27 +88,28 @@ fun <T> withoutDatabaseAccess(block: () -> T): T {
val contextDatabaseOrNull: CordaPersistence? get() = _contextDatabase.get() val contextDatabaseOrNull: CordaPersistence? get() = _contextDatabase.get()
class CordaPersistence( class CordaPersistence(
databaseConfig: DatabaseConfig, exportHibernateJMXStatistics: Boolean,
schemas: Set<MappedSchema>, schemas: Set<MappedSchema>,
val jdbcUrl: String, val jdbcUrl: String,
cacheFactory: NamedCacheFactory, cacheFactory: NamedCacheFactory,
attributeConverters: Collection<AttributeConverter<*, *>> = emptySet(), attributeConverters: Collection<AttributeConverter<*, *>> = emptySet(),
customClassLoader: ClassLoader? = null, customClassLoader: ClassLoader? = null,
val closeConnection: Boolean = true, val closeConnection: Boolean = true,
val errorHandler: DatabaseTransaction.(e: Exception) -> Unit = {} val errorHandler: DatabaseTransaction.(e: Exception) -> Unit = {},
allowHibernateToManageAppSchema: Boolean = false
) : Closeable { ) : Closeable {
companion object { companion object {
private val log = contextLogger() private val log = contextLogger()
} }
private val defaultIsolationLevel = databaseConfig.transactionIsolationLevel private val defaultIsolationLevel = TransactionIsolationLevel.default
val hibernateConfig: HibernateConfiguration by lazy { val hibernateConfig: HibernateConfiguration by lazy {
transaction { transaction {
try { try {
HibernateConfiguration(schemas, databaseConfig, attributeConverters, jdbcUrl, cacheFactory, customClassLoader) HibernateConfiguration(schemas, exportHibernateJMXStatistics, attributeConverters, jdbcUrl, cacheFactory, customClassLoader, allowHibernateToManageAppSchema)
} catch (e: Exception) { } catch (e: Exception) {
when (e) { when (e) {
is SchemaManagementException -> throw HibernateSchemaChangeException("Incompatible schema change detected. Please run the node with database.initialiseSchema=true. Reason: ${e.message}", e) is SchemaManagementException -> throw HibernateSchemaChangeException("Incompatible schema change detected. Please run schema migration scripts (node with sub-command run-migration-scripts). Reason: ${e.message}", e)
else -> throw HibernateConfigException("Could not create Hibernate configuration: ${e.message}", e) else -> throw HibernateConfigException("Could not create Hibernate configuration: ${e.message}", e)
} }
} }

View File

@ -19,11 +19,12 @@ import javax.persistence.AttributeConverter
class HibernateConfiguration( class HibernateConfiguration(
schemas: Set<MappedSchema>, schemas: Set<MappedSchema>,
private val databaseConfig: DatabaseConfig, private val exportHibernateJMXStatistics: Boolean,
private val attributeConverters: Collection<AttributeConverter<*, *>>, private val attributeConverters: Collection<AttributeConverter<*, *>>,
jdbcUrl: String, jdbcUrl: String,
cacheFactory: NamedCacheFactory, cacheFactory: NamedCacheFactory,
val customClassLoader: ClassLoader? = null val customClassLoader: ClassLoader? = null,
val allowHibernateToManageAppSchema: Boolean = false
) { ) {
companion object { companion object {
private val logger = contextLogger() private val logger = contextLogger()
@ -64,10 +65,10 @@ class HibernateConfiguration(
fun sessionFactoryForSchemas(key: Set<MappedSchema>): SessionFactory = sessionFactories.get(key, ::makeSessionFactoryForSchemas)!! fun sessionFactoryForSchemas(key: Set<MappedSchema>): SessionFactory = sessionFactories.get(key, ::makeSessionFactoryForSchemas)!!
private fun makeSessionFactoryForSchemas(schemas: Set<MappedSchema>): SessionFactory { private fun makeSessionFactoryForSchemas(schemas: Set<MappedSchema>): SessionFactory {
val sessionFactory = sessionFactoryFactory.makeSessionFactoryForSchemas(databaseConfig, schemas, customClassLoader, attributeConverters) val sessionFactory = sessionFactoryFactory.makeSessionFactoryForSchemas(schemas, customClassLoader, attributeConverters, allowHibernateToManageAppSchema)
// export Hibernate JMX statistics // export Hibernate JMX statistics
if (databaseConfig.exportHibernateJMXStatistics) if (exportHibernateJMXStatistics)
initStatistics(sessionFactory) initStatistics(sessionFactory)
return sessionFactory return sessionFactory
@ -75,7 +76,7 @@ class HibernateConfiguration(
// NOTE: workaround suggested to overcome deprecation of StatisticsService (since Hibernate v4.0) // NOTE: workaround suggested to overcome deprecation of StatisticsService (since Hibernate v4.0)
// https://stackoverflow.com/questions/23606092/hibernate-upgrade-statisticsservice // https://stackoverflow.com/questions/23606092/hibernate-upgrade-statisticsservice
fun initStatistics(sessionFactory: SessionFactory) { private fun initStatistics(sessionFactory: SessionFactory) {
val statsName = ObjectName("org.hibernate:type=statistics") val statsName = ObjectName("org.hibernate:type=statistics")
val mbeanServer = ManagementFactory.getPlatformMBeanServer() val mbeanServer = ManagementFactory.getPlatformMBeanServer()

View File

@ -0,0 +1,8 @@
package net.corda.nodeapi.internal.persistence
import liquibase.database.Database
import liquibase.database.jvm.JdbcConnection
interface LiquibaseDatabaseFactory {
fun getLiquibaseDatabase(conn: JdbcConnection): Database
}

View File

@ -0,0 +1,11 @@
package net.corda.nodeapi.internal.persistence
import liquibase.database.Database
import liquibase.database.DatabaseFactory
import liquibase.database.jvm.JdbcConnection
class LiquibaseDatabaseFactoryImpl : LiquibaseDatabaseFactory {
override fun getLiquibaseDatabase(conn: JdbcConnection): Database {
return DatabaseFactory.getInstance().findCorrectDatabaseImplementation(conn)
}
}

View File

@ -4,44 +4,40 @@ import com.fasterxml.jackson.databind.ObjectMapper
import liquibase.Contexts import liquibase.Contexts
import liquibase.LabelExpression import liquibase.LabelExpression
import liquibase.Liquibase import liquibase.Liquibase
import liquibase.database.Database
import liquibase.database.DatabaseFactory
import liquibase.database.jvm.JdbcConnection import liquibase.database.jvm.JdbcConnection
import liquibase.exception.LiquibaseException
import liquibase.resource.ClassLoaderResourceAccessor import liquibase.resource.ClassLoaderResourceAccessor
import net.corda.core.identity.CordaX500Name import net.corda.core.identity.CordaX500Name
import net.corda.nodeapi.internal.MigrationHelpers.getMigrationResource
import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.MappedSchema
import net.corda.core.utilities.contextLogger import net.corda.core.utilities.contextLogger
import net.corda.nodeapi.internal.MigrationHelpers.getMigrationResource
import net.corda.nodeapi.internal.cordapp.CordappLoader import net.corda.nodeapi.internal.cordapp.CordappLoader
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.InputStream import java.io.InputStream
import java.nio.file.Path import java.nio.file.Path
import java.sql.Statement import java.sql.Connection
import javax.sql.DataSource
import java.util.concurrent.locks.ReentrantLock import java.util.concurrent.locks.ReentrantLock
import javax.sql.DataSource
import kotlin.concurrent.withLock import kotlin.concurrent.withLock
// Migrate the database to the current version, using liquibase. // Migrate the database to the current version, using liquibase.
class SchemaMigration( open class SchemaMigration(
val schemas: Set<MappedSchema>,
val dataSource: DataSource, val dataSource: DataSource,
private val databaseConfig: DatabaseConfig,
cordappLoader: CordappLoader? = null, cordappLoader: CordappLoader? = null,
private val currentDirectory: Path?, private val currentDirectory: Path?,
// This parameter is used by the vault state migration to establish what the node's legal identity is when setting up // This parameter is used by the vault state migration to establish what the node's legal identity is when setting up
// its copy of the identity service. It is passed through using a system property. When multiple identity support is added, this will need // its copy of the identity service. It is passed through using a system property. When multiple identity support is added, this will need
// reworking so that multiple identities can be passed to the migration. // reworking so that multiple identities can be passed to the migration.
private val ourName: CordaX500Name? = null, private val ourName: CordaX500Name? = null,
// This parameter forces an error to be thrown if there are missing migrations. When using H2, Hibernate will automatically create schemas where they are protected val databaseFactory: LiquibaseDatabaseFactory = LiquibaseDatabaseFactoryImpl()) {
// missing, so no need to throw unless you're specifically testing whether all the migrations are present.
private val forceThrowOnMissingMigration: Boolean = false) {
companion object { companion object {
private val logger = contextLogger() private val logger = contextLogger()
const val NODE_BASE_DIR_KEY = "liquibase.nodeDaseDir" const val NODE_BASE_DIR_KEY = "liquibase.nodeDaseDir"
const val NODE_X500_NAME = "liquibase.nodeName" const val NODE_X500_NAME = "liquibase.nodeName"
val loader = ThreadLocal<CordappLoader>() val loader = ThreadLocal<CordappLoader>()
private val mutex = ReentrantLock() @JvmStatic
protected val mutex = ReentrantLock()
} }
init { init {
@ -50,36 +46,86 @@ class SchemaMigration(
private val classLoader = cordappLoader?.appClassLoader ?: Thread.currentThread().contextClassLoader private val classLoader = cordappLoader?.appClassLoader ?: Thread.currentThread().contextClassLoader
/**
* Main entry point to the schema migration.
* Called during node startup.
*/
fun nodeStartup(existingCheckpoints: Boolean) {
when {
databaseConfig.initialiseSchema -> {
migrateOlderDatabaseToUseLiquibase(existingCheckpoints)
runMigration(existingCheckpoints)
}
else -> checkState()
}
}
/** /**
* Will run the Liquibase migration on the actual database. * Will run the Liquibase migration on the actual database.
* @param existingCheckpoints Whether checkpoints exist that would prohibit running a migration
* @param schemas The set of MappedSchemas to check
* @param forceThrowOnMissingMigration throws an exception if a mapped schema is missing the migration resource. Can be set to false
* when allowing hibernate to create missing schemas in dev or tests.
*/ */
private fun runMigration(existingCheckpoints: Boolean) = doRunMigration(run = true, check = false, existingCheckpoints = existingCheckpoints) fun runMigration(existingCheckpoints: Boolean, schemas: Set<MappedSchema>, forceThrowOnMissingMigration: Boolean) {
val resourcesAndSourceInfo = prepareResources(schemas, forceThrowOnMissingMigration)
// current version of Liquibase appears to be non-threadsafe
// this is apparent when multiple in-process nodes are all running migrations simultaneously
mutex.withLock {
dataSource.connection.use { connection ->
val (runner, _, shouldBlockOnCheckpoints) = prepareRunner(connection, resourcesAndSourceInfo)
if (shouldBlockOnCheckpoints && existingCheckpoints)
throw CheckpointsException()
try {
runner.update(Contexts().toString())
} catch (exp: LiquibaseException) {
throw DatabaseMigrationException(exp.message, exp)
}
}
}
}
/** /**
* Ensures that the database is up to date with the latest migration changes. * Ensures that the database is up to date with the latest migration changes.
* @param schemas The set of MappedSchemas to check
* @param forceThrowOnMissingMigration throws an exception if a mapped schema is missing the migration resource. Can be set to false
* when allowing hibernate to create missing schemas in dev or tests.
*/ */
private fun checkState() = doRunMigration(run = false, check = true) fun checkState(schemas: Set<MappedSchema>, forceThrowOnMissingMigration: Boolean) {
val resourcesAndSourceInfo = prepareResources(schemas, forceThrowOnMissingMigration)
/** Create a resourse accessor that aggregates the changelogs included in the schemas into one dynamic stream. */ // current version of Liquibase appears to be non-threadsafe
private class CustomResourceAccessor(val dynamicInclude: String, val changelogList: List<String?>, classLoader: ClassLoader) : ClassLoaderResourceAccessor(classLoader) { // this is apparent when multiple in-process nodes are all running migrations simultaneously
mutex.withLock {
dataSource.connection.use { connection ->
val (_, changeToRunCount, _) = prepareRunner(connection, resourcesAndSourceInfo)
if (changeToRunCount > 0)
throw OutstandingDatabaseChangesException(changeToRunCount)
}
}
}
/**
* Synchronises the changelog table with the schema descriptions passed in without applying any of the changes to the database.
* This can be used when migrating a CorDapp that had its schema generated by hibernate to liquibase schema migration, or when
* updating from a version of Corda that does not use liquibase for CorDapps
* **Warning** - this will not check if the matching schema changes have been applied, it will just generate the changelog
* It must not be run on a newly installed CorDapp.
* @param schemas The set of schemas to add to the changelog
* @param forceThrowOnMissingMigration throw an exception if a mapped schema is missing its migration resource
*/
fun synchroniseSchemas(schemas: Set<MappedSchema>, forceThrowOnMissingMigration: Boolean) {
val resourcesAndSourceInfo = prepareResources(schemas, forceThrowOnMissingMigration)
// current version of Liquibase appears to be non-threadsafe
// this is apparent when multiple in-process nodes are all running migrations simultaneously
mutex.withLock {
dataSource.connection.use { connection ->
val (runner, _, _) = prepareRunner(connection, resourcesAndSourceInfo)
try {
runner.changeLogSync(Contexts().toString())
} catch (exp: LiquibaseException) {
throw DatabaseMigrationException(exp.message, exp)
}
}
}
}
/** Create a resource accessor that aggregates the changelogs included in the schemas into one dynamic stream. */
protected class CustomResourceAccessor(val dynamicInclude: String, val changelogList: List<String?>, classLoader: ClassLoader) :
ClassLoaderResourceAccessor(classLoader) {
override fun getResourcesAsStream(path: String): Set<InputStream> { override fun getResourcesAsStream(path: String): Set<InputStream> {
if (path == dynamicInclude) { if (path == dynamicInclude) {
// Create a map in Liquibase format including all migration files. // Create a map in Liquibase format including all migration files.
val includeAllFiles = mapOf("databaseChangeLog" to changelogList.filter { it != null }.map { file -> mapOf("include" to mapOf("file" to file)) }) val includeAllFiles = mapOf("databaseChangeLog"
to changelogList.filterNotNull().map { file -> mapOf("include" to mapOf("file" to file)) })
// Transform it to json. // Transform it to json.
val includeAllFilesJson = ObjectMapper().writeValueAsBytes(includeAllFiles) val includeAllFilesJson = ObjectMapper().writeValueAsBytes(includeAllFiles)
@ -91,7 +137,7 @@ class SchemaMigration(
} }
} }
private fun logOrThrowMigrationError(mappedSchema: MappedSchema): String? = private fun logOrThrowMigrationError(mappedSchema: MappedSchema, forceThrowOnMissingMigration: Boolean): String? =
if (forceThrowOnMissingMigration) { if (forceThrowOnMissingMigration) {
throw MissingMigrationException(mappedSchema) throw MissingMigrationException(mappedSchema)
} else { } else {
@ -99,25 +145,16 @@ class SchemaMigration(
null null
} }
private fun doRunMigration(
run: Boolean,
check: Boolean,
existingCheckpoints: Boolean? = null
) {
// Virtual file name of the changelog that includes all schemas. // Virtual file name of the changelog that includes all schemas.
val dynamicInclude = "master.changelog.json" val dynamicInclude = "master.changelog.json"
dataSource.connection.use { connection -> protected fun prepareResources(schemas: Set<MappedSchema>, forceThrowOnMissingMigration: Boolean): List<Pair<CustomResourceAccessor, String>> {
// Collect all changelog files referenced in the included schemas. // Collect all changelog files referenced in the included schemas.
val changelogList = schemas.mapNotNull { mappedSchema -> val changelogList = schemas.mapNotNull { mappedSchema ->
val resource = getMigrationResource(mappedSchema, classLoader) val resource = getMigrationResource(mappedSchema, classLoader)
when { when {
resource != null -> resource resource != null -> resource
// Corda OS FinanceApp in v3 has no Liquibase script, so no error is raised else -> logOrThrowMigrationError(mappedSchema, forceThrowOnMissingMigration)
(mappedSchema::class.qualifiedName == "net.corda.finance.schemas.CashSchemaV1" || mappedSchema::class.qualifiedName == "net.corda.finance.schemas.CommercialPaperSchemaV1") && mappedSchema.migrationResource == null -> null
else -> logOrThrowMigrationError(mappedSchema)
} }
} }
@ -130,112 +167,16 @@ class SchemaMigration(
} }
val customResourceAccessor = CustomResourceAccessor(dynamicInclude, changelogList, classLoader) val customResourceAccessor = CustomResourceAccessor(dynamicInclude, changelogList, classLoader)
checkResourcesInClassPath(changelogList) checkResourcesInClassPath(changelogList)
return listOf(Pair(customResourceAccessor, ""))
}
// current version of Liquibase appears to be non-threadsafe protected fun prepareRunner(connection: Connection,
// this is apparent when multiple in-process nodes are all running migrations simultaneously resourcesAndSourceInfo: List<Pair<CustomResourceAccessor, String>>): Triple<Liquibase, Int, Boolean> {
mutex.withLock { require(resourcesAndSourceInfo.size == 1)
val liquibase = Liquibase(dynamicInclude, customResourceAccessor, getLiquibaseDatabase(JdbcConnection(connection))) val liquibase = Liquibase(dynamicInclude, resourcesAndSourceInfo.single().first, databaseFactory.getLiquibaseDatabase(JdbcConnection(connection)))
val unRunChanges = liquibase.listUnrunChangeSets(Contexts(), LabelExpression()) val unRunChanges = liquibase.listUnrunChangeSets(Contexts(), LabelExpression())
return Triple(liquibase, unRunChanges.size, !unRunChanges.isEmpty())
when {
(run && !check) && (unRunChanges.isNotEmpty() && existingCheckpoints!!) -> throw CheckpointsException() // Do not allow database migration when there are checkpoints
run && !check -> liquibase.update(Contexts())
check && !run && unRunChanges.isNotEmpty() -> throw OutstandingDatabaseChangesException(unRunChanges.size)
check && !run -> {
} // Do nothing will be interpreted as "check succeeded"
else -> throw IllegalStateException("Invalid usage.")
}
}
}
}
private fun getLiquibaseDatabase(conn: JdbcConnection): Database {
return DatabaseFactory.getInstance().findCorrectDatabaseImplementation(conn)
}
/** For existing database created before verions 4.0 add Liquibase support - creates DATABASECHANGELOG and DATABASECHANGELOGLOCK tables and marks changesets as executed. */
private fun migrateOlderDatabaseToUseLiquibase(existingCheckpoints: Boolean): Boolean {
val isFinanceAppWithLiquibase = schemas.any { schema ->
(schema::class.qualifiedName == "net.corda.finance.schemas.CashSchemaV1"
|| schema::class.qualifiedName == "net.corda.finance.schemas.CommercialPaperSchemaV1")
&& schema.migrationResource != null
}
val noLiquibaseEntryLogForFinanceApp: (Statement) -> Boolean = {
it.execute("SELECT COUNT(*) FROM DATABASECHANGELOG WHERE FILENAME IN ('migration/cash.changelog-init.xml','migration/commercial-paper.changelog-init.xml')")
if (it.resultSet.next())
it.resultSet.getInt(1) == 0
else
true
}
val (isExistingDBWithoutLiquibase, isFinanceAppWithLiquibaseNotMigrated) = dataSource.connection.use {
val existingDatabase = it.metaData.getTables(null, null, "NODE%", null).next()
// Lower case names for PostgreSQL
|| it.metaData.getTables(null, null, "node%", null).next()
val hasLiquibase = it.metaData.getTables(null, null, "DATABASECHANGELOG%", null).next()
// Lower case names for PostgreSQL
|| it.metaData.getTables(null, null, "databasechangelog%", null).next()
val isFinanceAppWithLiquibaseNotMigrated = isFinanceAppWithLiquibase // If Finance App is pre v4.0 then no need to migrate it so no need to check.
&& existingDatabase
&& (!hasLiquibase // Migrate as other tables.
|| (hasLiquibase && it.createStatement().use { noLiquibaseEntryLogForFinanceApp(it) })) // If Liquibase is already in the database check if Finance App schema log is missing.
Pair(existingDatabase && !hasLiquibase, isFinanceAppWithLiquibaseNotMigrated)
}
if (isExistingDBWithoutLiquibase && existingCheckpoints)
throw CheckpointsException()
// Schema migrations pre release 4.0
val preV4Baseline = mutableListOf<String>()
if (isExistingDBWithoutLiquibase) {
preV4Baseline.addAll(listOf("migration/common.changelog-init.xml",
"migration/node-info.changelog-init.xml",
"migration/node-info.changelog-v1.xml",
"migration/node-info.changelog-v2.xml",
"migration/node-core.changelog-init.xml",
"migration/node-core.changelog-v3.xml",
"migration/node-core.changelog-v4.xml",
"migration/node-core.changelog-v5.xml",
"migration/node-core.changelog-pkey.xml",
"migration/vault-schema.changelog-init.xml",
"migration/vault-schema.changelog-v3.xml",
"migration/vault-schema.changelog-v4.xml",
"migration/vault-schema.changelog-pkey.xml"))
if (schemas.any { schema -> schema.migrationResource == "node-notary.changelog-master" })
preV4Baseline.addAll(listOf("migration/node-notary.changelog-init.xml",
"migration/node-notary.changelog-v1.xml"))
if (schemas.any { schema -> schema.migrationResource == "notary-raft.changelog-master" })
preV4Baseline.addAll(listOf("migration/notary-raft.changelog-init.xml",
"migration/notary-raft.changelog-v1.xml"))
if (schemas.any { schema -> schema.migrationResource == "notary-bft-smart.changelog-master" })
preV4Baseline.addAll(listOf("migration/notary-bft-smart.changelog-init.xml",
"migration/notary-bft-smart.changelog-v1.xml"))
}
if (isFinanceAppWithLiquibaseNotMigrated) {
preV4Baseline.addAll(listOf("migration/cash.changelog-init.xml",
"migration/cash.changelog-v1.xml",
"migration/commercial-paper.changelog-init.xml",
"migration/commercial-paper.changelog-v1.xml"))
}
if (preV4Baseline.isNotEmpty()) {
val dynamicInclude = "master.changelog.json" // Virtual file name of the changelog that includes all schemas.
checkResourcesInClassPath(preV4Baseline)
dataSource.connection.use { connection ->
val customResourceAccessor = CustomResourceAccessor(dynamicInclude, preV4Baseline, classLoader)
val liquibase = Liquibase(dynamicInclude, customResourceAccessor, getLiquibaseDatabase(JdbcConnection(connection)))
liquibase.changeLogSync(Contexts(), LabelExpression())
}
}
return isExistingDBWithoutLiquibase || isFinanceAppWithLiquibaseNotMigrated
} }
private fun checkResourcesInClassPath(resources: List<String?>) { private fun checkResourcesInClassPath(resources: List<String?>) {
@ -247,7 +188,7 @@ class SchemaMigration(
} }
} }
open class DatabaseMigrationException(message: String) : IllegalArgumentException(message) { open class DatabaseMigrationException(message: String?, cause: Throwable? = null) : IllegalArgumentException(message, cause) {
override val message: String = super.message!! override val message: String = super.message!!
} }
@ -269,6 +210,6 @@ class CheckpointsException : DatabaseMigrationException("Attempting to update th
class DatabaseIncompatibleException(@Suppress("MemberVisibilityCanBePrivate") private val reason: String) : DatabaseMigrationException(errorMessageFor(reason)) { class DatabaseIncompatibleException(@Suppress("MemberVisibilityCanBePrivate") private val reason: String) : DatabaseMigrationException(errorMessageFor(reason)) {
internal companion object { internal companion object {
fun errorMessageFor(reason: String): String = "Incompatible database schema version detected, please run the node with configuration option database.initialiseSchema=true. Reason: $reason" fun errorMessageFor(reason: String): String = "Incompatible database schema version detected, please run schema migration scripts (node with sub-command run-migration-scripts). Reason: $reason"
} }
} }

View File

@ -3,9 +3,8 @@ package net.corda.nodeapi.internal.persistence.factory
import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.MappedSchema
import net.corda.core.utilities.contextLogger import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.toHexString import net.corda.core.utilities.toHexString
import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.nodeapi.internal.persistence.HibernateConfiguration import net.corda.nodeapi.internal.persistence.HibernateConfiguration
import net.corda.nodeapi.internal.persistence.SchemaInitializationType import net.corda.nodeapi.internal.persistence.TransactionIsolationLevel
import org.hibernate.SessionFactory import org.hibernate.SessionFactory
import org.hibernate.boot.Metadata import org.hibernate.boot.Metadata
import org.hibernate.boot.MetadataBuilder import org.hibernate.boot.MetadataBuilder
@ -26,22 +25,19 @@ abstract class BaseSessionFactoryFactory : CordaSessionFactoryFactory {
private val logger = contextLogger() private val logger = contextLogger()
} }
open fun buildHibernateConfig(databaseConfig: DatabaseConfig, metadataSources: MetadataSources): Configuration { open fun buildHibernateConfig(metadataSources: MetadataSources, allowHibernateToManageAppSchema: Boolean): Configuration {
val hbm2dll: String = val hbm2dll: String =
if (databaseConfig.initialiseSchema && databaseConfig.initialiseAppSchema == SchemaInitializationType.UPDATE) { if (allowHibernateToManageAppSchema) {
"update" "update"
} else if ((!databaseConfig.initialiseSchema && databaseConfig.initialiseAppSchema == SchemaInitializationType.UPDATE)
|| databaseConfig.initialiseAppSchema == SchemaInitializationType.VALIDATE) {
"validate"
} else { } else {
"none" "validate"
} }
// We set a connection provider as the auto schema generation requires it. The auto schema generation will not // We set a connection provider as the auto schema generation requires it. The auto schema generation will not
// necessarily remain and would likely be replaced by something like Liquibase. For now it is very convenient though. // necessarily remain and would likely be replaced by something like Liquibase. For now it is very convenient though.
return Configuration(metadataSources).setProperty("hibernate.connection.provider_class", HibernateConfiguration.NodeDatabaseConnectionProvider::class.java.name) return Configuration(metadataSources).setProperty("hibernate.connection.provider_class", HibernateConfiguration.NodeDatabaseConnectionProvider::class.java.name)
.setProperty("hibernate.format_sql", "true") .setProperty("hibernate.format_sql", "true")
.setProperty("javax.persistence.validation.mode", "none") .setProperty("javax.persistence.validation.mode", "none")
.setProperty("hibernate.connection.isolation", databaseConfig.transactionIsolationLevel.jdbcValue.toString()) .setProperty("hibernate.connection.isolation", TransactionIsolationLevel.default.jdbcValue.toString())
.setProperty("hibernate.hbm2ddl.auto", hbm2dll) .setProperty("hibernate.hbm2ddl.auto", hbm2dll)
.setProperty("hibernate.jdbc.time_zone", "UTC") .setProperty("hibernate.jdbc.time_zone", "UTC")
} }
@ -85,15 +81,15 @@ abstract class BaseSessionFactoryFactory : CordaSessionFactoryFactory {
} }
final override fun makeSessionFactoryForSchemas( final override fun makeSessionFactoryForSchemas(
databaseConfig: DatabaseConfig,
schemas: Set<MappedSchema>, schemas: Set<MappedSchema>,
customClassLoader: ClassLoader?, customClassLoader: ClassLoader?,
attributeConverters: Collection<AttributeConverter<*, *>>): SessionFactory { attributeConverters: Collection<AttributeConverter<*, *>>,
allowHibernateToMananageAppSchema: Boolean): SessionFactory {
logger.info("Creating session factory for schemas: $schemas") logger.info("Creating session factory for schemas: $schemas")
val serviceRegistry = BootstrapServiceRegistryBuilder().build() val serviceRegistry = BootstrapServiceRegistryBuilder().build()
val metadataSources = MetadataSources(serviceRegistry) val metadataSources = MetadataSources(serviceRegistry)
val config = buildHibernateConfig(databaseConfig, metadataSources) val config = buildHibernateConfig(metadataSources, allowHibernateToMananageAppSchema)
schemas.forEach { schema -> schemas.forEach { schema ->
schema.mappedTypes.forEach { config.addAnnotatedClass(it) } schema.mappedTypes.forEach { config.addAnnotatedClass(it) }
} }

View File

@ -1,7 +1,6 @@
package net.corda.nodeapi.internal.persistence.factory package net.corda.nodeapi.internal.persistence.factory
import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.MappedSchema
import net.corda.nodeapi.internal.persistence.DatabaseConfig
import org.hibernate.SessionFactory import org.hibernate.SessionFactory
import org.hibernate.boot.Metadata import org.hibernate.boot.Metadata
import org.hibernate.boot.MetadataBuilder import org.hibernate.boot.MetadataBuilder
@ -11,10 +10,10 @@ interface CordaSessionFactoryFactory {
val databaseType: String val databaseType: String
fun canHandleDatabase(jdbcUrl: String): Boolean fun canHandleDatabase(jdbcUrl: String): Boolean
fun makeSessionFactoryForSchemas( fun makeSessionFactoryForSchemas(
databaseConfig: DatabaseConfig,
schemas: Set<MappedSchema>, schemas: Set<MappedSchema>,
customClassLoader: ClassLoader?, customClassLoader: ClassLoader?,
attributeConverters: Collection<AttributeConverter<*, *>>): SessionFactory attributeConverters: Collection<AttributeConverter<*, *>>,
allowHibernateToMananageAppSchema: Boolean): SessionFactory
fun getExtraConfiguration(key: String): Any? fun getExtraConfiguration(key: String): Any?
fun buildHibernateMetadata(metadataBuilder: MetadataBuilder, attributeConverters: Collection<AttributeConverter<*, *>>): Metadata fun buildHibernateMetadata(metadataBuilder: MetadataBuilder, attributeConverters: Collection<AttributeConverter<*, *>>): Metadata
} }

View File

@ -200,10 +200,7 @@ internal fun createClientSslHelper(target: NetworkHostAndPort,
expectedRemoteLegalNames: Set<CordaX500Name>, expectedRemoteLegalNames: Set<CordaX500Name>,
keyManagerFactory: KeyManagerFactory, keyManagerFactory: KeyManagerFactory,
trustManagerFactory: TrustManagerFactory): SslHandler { trustManagerFactory: TrustManagerFactory): SslHandler {
val sslContext = SSLContext.getInstance("TLS") val sslContext = createAndInitSslContext(keyManagerFactory, trustManagerFactory)
val keyManagers = keyManagerFactory.keyManagers
val trustManagers = trustManagerFactory.trustManagers.filterIsInstance(X509ExtendedTrustManager::class.java).map { LoggingTrustManagerWrapper(it) }.toTypedArray()
sslContext.init(keyManagers, trustManagers, newSecureRandom())
val sslEngine = sslContext.createSSLEngine(target.host, target.port) val sslEngine = sslContext.createSSLEngine(target.host, target.port)
sslEngine.useClientMode = true sslEngine.useClientMode = true
sslEngine.enabledProtocols = ArtemisTcpTransport.TLS_VERSIONS.toTypedArray() sslEngine.enabledProtocols = ArtemisTcpTransport.TLS_VERSIONS.toTypedArray()
@ -239,10 +236,7 @@ internal fun createClientOpenSslHandler(target: NetworkHostAndPort,
internal fun createServerSslHandler(keyStore: CertificateStore, internal fun createServerSslHandler(keyStore: CertificateStore,
keyManagerFactory: KeyManagerFactory, keyManagerFactory: KeyManagerFactory,
trustManagerFactory: TrustManagerFactory): SslHandler { trustManagerFactory: TrustManagerFactory): SslHandler {
val sslContext = SSLContext.getInstance("TLS") val sslContext = createAndInitSslContext(keyManagerFactory, trustManagerFactory)
val keyManagers = keyManagerFactory.keyManagers
val trustManagers = trustManagerFactory.trustManagers.filterIsInstance(X509ExtendedTrustManager::class.java).map { LoggingTrustManagerWrapper(it) }.toTypedArray()
sslContext.init(keyManagers, trustManagers, newSecureRandom())
val sslEngine = sslContext.createSSLEngine() val sslEngine = sslContext.createSSLEngine()
sslEngine.useClientMode = false sslEngine.useClientMode = false
sslEngine.needClientAuth = true sslEngine.needClientAuth = true
@ -256,6 +250,15 @@ internal fun createServerSslHandler(keyStore: CertificateStore,
return SslHandler(sslEngine, false, LoggingImmediateExecutor) return SslHandler(sslEngine, false, LoggingImmediateExecutor)
} }
fun createAndInitSslContext(keyManagerFactory: KeyManagerFactory, trustManagerFactory: TrustManagerFactory): SSLContext {
val sslContext = SSLContext.getInstance("TLS")
val keyManagers = keyManagerFactory.keyManagers
val trustManagers = trustManagerFactory.trustManagers.filterIsInstance(X509ExtendedTrustManager::class.java)
.map { LoggingTrustManagerWrapper(it) }.toTypedArray()
sslContext.init(keyManagers, trustManagers, newSecureRandom())
return sslContext
}
@VisibleForTesting @VisibleForTesting
fun initialiseTrustStoreAndEnableCrlChecking(trustStore: CertificateStore, revocationConfig: RevocationConfig): ManagerFactoryParameters { fun initialiseTrustStoreAndEnableCrlChecking(trustStore: CertificateStore, revocationConfig: RevocationConfig): ManagerFactoryParameters {
val pkixParams = PKIXBuilderParameters(trustStore.value.internal, X509CertSelector()) val pkixParams = PKIXBuilderParameters(trustStore.value.internal, X509CertSelector())

View File

@ -14,7 +14,7 @@ class HibernateConfigurationFactoryLoadingTest {
val cacheFactory = mock<NamedCacheFactory>() val cacheFactory = mock<NamedCacheFactory>()
HibernateConfiguration( HibernateConfiguration(
emptySet(), emptySet(),
DatabaseConfig(), false,
emptyList(), emptyList(),
jdbcUrl, jdbcUrl,
cacheFactory) cacheFactory)

View File

@ -1,32 +0,0 @@
package net.corda.node.endurance
import net.corda.core.utilities.getOrThrow
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.BOB_NAME
import net.corda.testing.driver.DriverParameters
import net.corda.testing.driver.driver
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
@RunWith(Parameterized::class)
class NodesStartStopSingleVmTests(@Suppress("unused") private val iteration: Int) {
companion object {
@JvmStatic
@Parameterized.Parameters(name = "iteration = {0}")
fun iterations(): Iterable<Array<Int>> {
return (1..60).map { arrayOf(it) }
}
}
@Test(timeout = 300_000)
fun nodesStartStop() {
driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) {
val alice = startNode(providedName = ALICE_NAME)
val bob = startNode(providedName = BOB_NAME)
alice.getOrThrow()
bob.getOrThrow()
}
}
}

View File

@ -37,7 +37,8 @@ class FlowCheckpointVersionNodeStartupCheckTest {
startNodesInProcess = false, startNodesInProcess = false,
inMemoryDB = false, // Ensure database is persisted between node restarts so we can keep suspended flows inMemoryDB = false, // Ensure database is persisted between node restarts so we can keep suspended flows
cordappsForAllNodes = emptyList(), cordappsForAllNodes = emptyList(),
notarySpecs = emptyList() notarySpecs = emptyList(),
allowHibernateToManageAppSchema = false
)) { )) {
createSuspendedFlowInBob() createSuspendedFlowInBob()
val cordappsDir = baseDirectory(BOB_NAME) / "cordapps" val cordappsDir = baseDirectory(BOB_NAME) / "cordapps"

View File

@ -86,7 +86,7 @@ class NodeStatePersistenceTests {
nodeName nodeName
}() }()
val nodeHandle = startNode(providedName = nodeName, rpcUsers = listOf(user), customOverrides = mapOf("devMode" to "false")).getOrThrow() val nodeHandle = startNode(providedName = nodeName, rpcUsers = listOf(user)).getOrThrow()
val result = CordaRPCClient(nodeHandle.rpcAddress).start(user.username, user.password).use { val result = CordaRPCClient(nodeHandle.rpcAddress).start(user.username, user.password).use {
val page = it.proxy.vaultQuery(MessageState::class.java) val page = it.proxy.vaultQuery(MessageState::class.java)
page.states.singleOrNull() page.states.singleOrNull()

View File

@ -47,7 +47,7 @@ class DistributedServiceTests {
invokeRpc(CordaRPCOps::stateMachinesFeed)) invokeRpc(CordaRPCOps::stateMachinesFeed))
) )
driver(DriverParameters( driver(DriverParameters(
cordappsForAllNodes = FINANCE_CORDAPPS + cordappWithPackages("net.corda.notary.raft"), cordappsForAllNodes = FINANCE_CORDAPPS + cordappWithPackages(),
notarySpecs = listOf(NotarySpec( notarySpecs = listOf(NotarySpec(
DUMMY_NOTARY_NAME, DUMMY_NOTARY_NAME,
rpcUsers = listOf(testUser), rpcUsers = listOf(testUser),

View File

@ -84,7 +84,8 @@ class NodeRegistrationTest {
portAllocation = portAllocation, portAllocation = portAllocation,
compatibilityZone = compatibilityZone, compatibilityZone = compatibilityZone,
notarySpecs = listOf(NotarySpec(notaryName)), notarySpecs = listOf(NotarySpec(notaryName)),
notaryCustomOverrides = mapOf("devMode" to false) notaryCustomOverrides = mapOf("devMode" to false),
allowHibernateToManageAppSchema = false
) { ) {
startNode(providedName = aliceName, customOverrides = mapOf("devMode" to false)).getOrThrow() startNode(providedName = aliceName, customOverrides = mapOf("devMode" to false)).getOrThrow()

View File

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

View File

@ -0,0 +1,329 @@
package net.corda.node.amqp;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLEngineResult;
import javax.net.ssl.SSLEngineResult.HandshakeStatus;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSession;
import java.io.IOException;
import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.Arrays;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* A class that represents an SSL/TLS peer, and can be extended to create a client or a server.
* <p/>
* It makes use of the JSSE framework, and specifically the {@link SSLEngine} logic, which
* is described by Oracle as "an advanced API, not appropriate for casual use", since
* it requires the user to implement much of the communication establishment procedure himself.
* More information about it can be found here: http://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/JSSERefGuide.html#SSLEngine
* <p/>
* {@link NioSslPeer} implements the handshake protocol, required to establish a connection between two peers,
* which is common for both client and server and provides the abstract {@link NioSslPeer#read(SocketChannel, SSLEngine)} and
* {@link NioSslPeer#write(SocketChannel, SSLEngine, String)} methods, that need to be implemented by the specific SSL/TLS peer
* that is going to extend this class.
*
* @author <a href="mailto:alex.a.karnezis@gmail.com">Alex Karnezis</a>
*/
public abstract class NioSslPeer {
/**
* Class' logger.
*/
protected final Logger log = LoggerFactory.getLogger(getClass());
/**
* Will contain this peer's application data in plaintext, that will be later encrypted
* using {@link SSLEngine#wrap(ByteBuffer, ByteBuffer)} and sent to the other peer. This buffer can typically
* be of any size, as long as it is large enough to contain this peer's outgoing messages.
* If this peer tries to send a message bigger than buffer's capacity a {@link BufferOverflowException}
* will be thrown.
*/
protected ByteBuffer myAppData;
/**
* Will contain this peer's encrypted data, that will be generated after {@link SSLEngine#wrap(ByteBuffer, ByteBuffer)}
* is applied on {@link NioSslPeer#myAppData}. It should be initialized using {@link SSLSession#getPacketBufferSize()},
* which returns the size up to which, SSL/TLS packets will be generated from the engine under a session.
* All SSLEngine network buffers should be sized at least this large to avoid insufficient space problems when performing wrap and unwrap calls.
*/
protected ByteBuffer myNetData;
/**
* Will contain the other peer's (decrypted) application data. It must be large enough to hold the application data
* from any peer. Can be initialized with {@link SSLSession#getApplicationBufferSize()} for an estimation
* of the other peer's application data and should be enlarged if this size is not enough.
*/
protected ByteBuffer peerAppData;
/**
* Will contain the other peer's encrypted data. The SSL/TLS protocols specify that implementations should produce packets containing at most 16 KB of plaintext,
* so a buffer sized to this value should normally cause no capacity problems. However, some implementations violate the specification and generate large records up to 32 KB.
* If the {@link SSLEngine#unwrap(ByteBuffer, ByteBuffer)} detects large inbound packets, the buffer sizes returned by SSLSession will be updated dynamically, so the this peer
* should check for overflow conditions and enlarge the buffer using the session's (updated) buffer size.
*/
protected ByteBuffer peerNetData;
/**
* Will be used to execute tasks that may emerge during handshake in parallel with the server's main thread.
*/
protected ExecutorService executor = Executors.newSingleThreadExecutor();
protected abstract void read(SocketChannel socketChannel, SSLEngine engine) throws Exception;
protected abstract void write(SocketChannel socketChannel, SSLEngine engine, String message) throws Exception;
/**
* Implements the handshake protocol between two peers, required for the establishment of the SSL/TLS connection.
* During the handshake, encryption configuration information - such as the list of available cipher suites - will be exchanged
* and if the handshake is successful will lead to an established SSL/TLS session.
*
* <p/>
* A typical handshake will usually contain the following steps:
*
* <ul>
* <li>1. wrap: ClientHello</li>
* <li>2. unwrap: ServerHello/Cert/ServerHelloDone</li>
* <li>3. wrap: ClientKeyExchange</li>
* <li>4. wrap: ChangeCipherSpec</li>
* <li>5. wrap: Finished</li>
* <li>6. unwrap: ChangeCipherSpec</li>
* <li>7. unwrap: Finished</li>
* </ul>
* <p/>
* Handshake is also used during the end of the session, in order to properly close the connection between the two peers.
* A proper connection close will typically include the one peer sending a CLOSE message to another, and then wait for
* the other's CLOSE message to close the transport link. The other peer from his perspective would read a CLOSE message
* from his peer and then enter the handshake procedure to send his own CLOSE message as well.
*
* @param socketChannel - the socket channel that connects the two peers.
* @param engine - the engine that will be used for encryption/decryption of the data exchanged with the other peer.
* @return True if the connection handshake was successful or false if an error occurred.
* @throws IOException - if an error occurs during read/write to the socket channel.
*/
protected boolean doHandshake(SocketChannel socketChannel, SSLEngine engine) throws IOException {
log.debug("About to do handshake...");
SSLEngineResult result;
HandshakeStatus handshakeStatus;
// NioSslPeer's fields myAppData and peerAppData are supposed to be large enough to hold all message data the peer
// will send and expects to receive from the other peer respectively. Since the messages to be exchanged will usually be less
// than 16KB long the capacity of these fields should also be smaller. Here we initialize these two local buffers
// to be used for the handshake, while keeping client's buffers at the same size.
int appBufferSize = engine.getSession().getApplicationBufferSize();
ByteBuffer myAppData = ByteBuffer.allocate(appBufferSize);
ByteBuffer peerAppData = ByteBuffer.allocate(appBufferSize);
myNetData.clear();
peerNetData.clear();
handshakeStatus = engine.getHandshakeStatus();
while (handshakeStatus != SSLEngineResult.HandshakeStatus.FINISHED && handshakeStatus != SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING) {
switch (handshakeStatus) {
case NEED_UNWRAP:
if (socketChannel.read(peerNetData) < 0) {
if (engine.isInboundDone() && engine.isOutboundDone()) {
return false;
}
try {
engine.closeInbound();
} catch (SSLException e) {
log.error("This engine was forced to close inbound, without having received the proper SSL/TLS close " +
"notification message from the peer, due to end of stream.", e);
}
engine.closeOutbound();
// After closeOutbound the engine will be set to WRAP state, in order to try to send a close message to the client.
handshakeStatus = engine.getHandshakeStatus();
break;
}
peerNetData.flip();
try {
result = engine.unwrap(peerNetData, peerAppData);
peerNetData.compact();
handshakeStatus = result.getHandshakeStatus();
} catch (SSLException sslException) {
log.error("A problem was encountered while processing the data that caused the SSLEngine to abort." +
" Will try to properly close connection...", sslException);
engine.closeOutbound();
handshakeStatus = engine.getHandshakeStatus();
break;
}
switch (result.getStatus()) {
case OK:
break;
case BUFFER_OVERFLOW:
// Will occur when peerAppData's capacity is smaller than the data derived from peerNetData's unwrap.
peerAppData = enlargeApplicationBuffer(engine, peerAppData);
break;
case BUFFER_UNDERFLOW:
// Will occur either when no data was read from the peer or when the peerNetData buffer was too small to hold all peer's data.
peerNetData = handleBufferUnderflow(engine, peerNetData);
break;
case CLOSED:
if (engine.isOutboundDone()) {
return false;
} else {
engine.closeOutbound();
handshakeStatus = engine.getHandshakeStatus();
break;
}
default:
throw new IllegalStateException("Invalid SSL status: " + result.getStatus());
}
break;
case NEED_WRAP:
myNetData.clear();
try {
result = engine.wrap(myAppData, myNetData);
handshakeStatus = result.getHandshakeStatus();
} catch (SSLException sslException) {
log.error("A problem was encountered while processing the data that caused the SSLEngine to abort." +
"Will try to properly close connection...", sslException);
engine.closeOutbound();
handshakeStatus = engine.getHandshakeStatus();
break;
}
switch (result.getStatus()) {
case OK :
myNetData.flip();
while (myNetData.hasRemaining()) {
socketChannel.write(myNetData);
}
break;
case BUFFER_OVERFLOW:
// Will occur if there is not enough space in myNetData buffer to write all the data that would be generated by the method wrap.
// Since myNetData is set to session's packet size we should not get to this point because SSLEngine is supposed
// to produce messages smaller or equal to that, but a general handling would be the following:
myNetData = enlargePacketBuffer(engine, myNetData);
break;
case BUFFER_UNDERFLOW:
throw new SSLException("Buffer underflow occurred after a wrap. I don't think we should ever get here.");
case CLOSED:
try {
myNetData.flip();
while (myNetData.hasRemaining()) {
socketChannel.write(myNetData);
}
// At this point the handshake status will probably be NEED_UNWRAP so we make sure that peerNetData is clear to read.
peerNetData.clear();
} catch (Exception e) {
log.error("Failed to send server's CLOSE message due to socket channel's failure.");
handshakeStatus = engine.getHandshakeStatus();
}
break;
default:
throw new IllegalStateException("Invalid SSL status: " + result.getStatus());
}
break;
case NEED_TASK:
Runnable task;
while ((task = engine.getDelegatedTask()) != null) {
executor.execute(task);
}
handshakeStatus = engine.getHandshakeStatus();
break;
default:
throw new IllegalStateException("Invalid SSL status: " + handshakeStatus);
}
}
log.debug("Handshake status: " + handshakeStatus);
return true;
}
protected ByteBuffer enlargePacketBuffer(SSLEngine engine, ByteBuffer buffer) {
return enlargeBuffer(buffer, engine.getSession().getPacketBufferSize());
}
protected ByteBuffer enlargeApplicationBuffer(SSLEngine engine, ByteBuffer buffer) {
return enlargeBuffer(buffer, engine.getSession().getApplicationBufferSize());
}
/**
* Compares <code>sessionProposedCapacity<code> with buffer's capacity. If buffer's capacity is smaller,
* returns a buffer with the proposed capacity. If it's equal or larger, returns a buffer
* with capacity twice the size of the initial one.
*
* @param buffer - the buffer to be enlarged.
* @param sessionProposedCapacity - the minimum size of the new buffer, proposed by {@link SSLSession}.
* @return A new buffer with a larger capacity.
*/
protected ByteBuffer enlargeBuffer(ByteBuffer buffer, int sessionProposedCapacity) {
if (sessionProposedCapacity > buffer.capacity()) {
buffer = ByteBuffer.allocate(sessionProposedCapacity);
} else {
buffer = ByteBuffer.allocate(buffer.capacity() * 2);
}
return buffer;
}
/**
* Handles {@link SSLEngineResult.Status#BUFFER_UNDERFLOW}. Will check if the buffer is already filled, and if there is no space problem
* will return the same buffer, so the client tries to read again. If the buffer is already filled will try to enlarge the buffer either to
* session's proposed size or to a larger capacity. A buffer underflow can happen only after an unwrap, so the buffer will always be a
* peerNetData buffer.
*
* @param buffer - will always be peerNetData buffer.
* @param engine - the engine used for encryption/decryption of the data exchanged between the two peers.
* @return The same buffer if there is no space problem or a new buffer with the same data but more space.
*/
protected ByteBuffer handleBufferUnderflow(SSLEngine engine, ByteBuffer buffer) {
if (engine.getSession().getPacketBufferSize() < buffer.limit()) {
return buffer;
} else {
ByteBuffer replaceBuffer = enlargePacketBuffer(engine, buffer);
buffer.flip();
replaceBuffer.put(buffer);
return replaceBuffer;
}
}
/**
* This method should be called when this peer wants to explicitly close the connection
* or when a close message has arrived from the other peer, in order to provide an orderly shutdown.
* <p/>
* It first calls {@link SSLEngine#closeOutbound()} which prepares this peer to send its own close message and
* sets {@link SSLEngine} to the <code>NEED_WRAP</code> state. Then, it delegates the exchange of close messages
* to the handshake method and finally, it closes socket channel.
*
* @param socketChannel - the transport link used between the two peers.
* @param engine - the engine used for encryption/decryption of the data exchanged between the two peers.
* @throws IOException if an I/O error occurs to the socket channel.
*/
protected void closeConnection(SocketChannel socketChannel, SSLEngine engine) throws IOException {
engine.closeOutbound();
doHandshake(socketChannel, engine);
socketChannel.close();
}
/**
* In addition to orderly shutdowns, an unorderly shutdown may occur, when the transport link (socket channel)
* is severed before close messages are exchanged. This may happen by getting an -1 or {@link IOException}
* when trying to read from the socket channel, or an {@link IOException} when trying to write to it.
* In both cases {@link SSLEngine#closeInbound()} should be called and then try to follow the standard procedure.
*
* @param socketChannel - the transport link used between the two peers.
* @param engine - the engine used for encryption/decryption of the data exchanged between the two peers.
* @throws IOException if an I/O error occurs to the socket channel.
*/
protected void handleEndOfStream(SocketChannel socketChannel, SSLEngine engine) throws IOException {
try {
engine.closeInbound();
} catch (Exception e) {
log.error("This engine was forced to close inbound, without having received the proper SSL/TLS close notification message from the peer, due to end of stream.");
}
closeConnection(socketChannel, engine);
}
protected String peerAppDataAsString() {
return new String(Arrays.copyOf(peerAppData.array(), peerAppData.limit()));
}
}

View File

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

View File

@ -0,0 +1,69 @@
package net.corda.node.amqp;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.TrustManagerFactory;
import java.time.Duration;
/**
* This class provides a runnable that can be used to initialize a {@link NioSslServer} thread.
* <p/>
* Run starts the server, which will start listening to the configured IP address and port for
* new SSL/TLS connections and serve the ones already connected to it.
* <p/>
* Also a stop method is provided in order to gracefully close the server and stop the thread.
*
* @author <a href="mailto:alex.a.karnezis@gmail.com">Alex Karnezis</a>
*/
public class ServerThread implements AutoCloseable {
private final static Logger log = LoggerFactory.getLogger(ServerThread.class);
private static final long JOIN_TIMEOUT_MS = 10000;
private final NioSslServer server;
private Thread serverThread;
public ServerThread(KeyManagerFactory keyManagerFactory, TrustManagerFactory trustManagerFactory, int port) throws Exception {
this(keyManagerFactory, trustManagerFactory, port, null);
}
public ServerThread(KeyManagerFactory keyManagerFactory, TrustManagerFactory trustManagerFactory, int port, @Nullable Duration handshakeDelay) throws Exception {
server = new NioSslServer(keyManagerFactory, trustManagerFactory, "localhost", port, handshakeDelay);
}
public void start() {
Runnable serverRunnable = () -> {
try {
server.start();
} catch (Exception e) {
log.error("Exception starting server", e);
}
};
serverThread = new Thread(serverRunnable, this.getClass().getSimpleName() + "-ServerThread");
serverThread.start();
}
/**
* Should be called in order to gracefully stop the server.
*/
public void stop() throws InterruptedException {
server.stop();
serverThread.join(JOIN_TIMEOUT_MS);
}
public boolean isActive() {
return server.isActive();
}
@Override
public void close() throws Exception {
stop();
}
}

View File

@ -1,5 +1,6 @@
package net.corda.serialization.reproduction; package net.corda.serialization.reproduction;
import com.google.common.io.LineProcessor;
import net.corda.client.rpc.CordaRPCClient; import net.corda.client.rpc.CordaRPCClient;
import net.corda.core.concurrent.CordaFuture; import net.corda.core.concurrent.CordaFuture;
import net.corda.node.services.Permissions; import net.corda.node.services.Permissions;

View File

@ -44,7 +44,7 @@ class BootTests {
rpc.startFlow(::ObjectInputStreamFlow).returnValue.getOrThrow() rpc.startFlow(::ObjectInputStreamFlow).returnValue.getOrThrow()
} }
} }
driver(DriverParameters(cordappsForAllNodes = listOf(enclosedCordapp()))) { driver(DriverParameters(cordappsForAllNodes = listOf(enclosedCordapp()), allowHibernateToManageAppSchema = false)) {
val devModeNode = startNode(devParams).getOrThrow() val devModeNode = startNode(devParams).getOrThrow()
val node = startNode(ALICE_NAME, devMode = false, parameters = params).getOrThrow() val node = startNode(ALICE_NAME, devMode = false, parameters = params).getOrThrow()

View File

@ -15,6 +15,7 @@ import net.corda.testing.core.singleIdentity
import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.DriverParameters
import net.corda.testing.driver.driver import net.corda.testing.driver.driver
import net.corda.testing.node.User import net.corda.testing.node.User
import net.corda.testing.node.internal.enclosedCordapp
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.junit.Test import org.junit.Test
@ -23,7 +24,7 @@ class CordappScanningDriverTest {
fun `sub-classed initiated flow pointing to the same initiating flow as its super-class`() { fun `sub-classed initiated flow pointing to the same initiating flow as its super-class`() {
val user = User("u", "p", setOf(startFlow<ReceiveFlow>())) val user = User("u", "p", setOf(startFlow<ReceiveFlow>()))
// The driver will automatically pick up the annotated flows below // The driver will automatically pick up the annotated flows below
driver(DriverParameters(notarySpecs = emptyList())) { driver(DriverParameters(notarySpecs = emptyList(), cordappsForAllNodes = listOf(enclosedCordapp()))) {
val (alice, bob) = listOf( val (alice, bob) = listOf(
startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)), startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)),
startNode(providedName = BOB_NAME)).transpose().getOrThrow() startNode(providedName = BOB_NAME)).transpose().getOrThrow()

View File

@ -17,7 +17,7 @@ import javax.security.auth.x500.X500Principal
class NodeKeystoreCheckTest { class NodeKeystoreCheckTest {
@Test(timeout=300_000) @Test(timeout=300_000)
fun `starting node in non-dev mode with no key store`() { fun `starting node in non-dev mode with no key store`() {
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = emptyList())) { driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = emptyList(), allowHibernateToManageAppSchema = false)) {
assertThatThrownBy { assertThatThrownBy {
startNode(customOverrides = mapOf("devMode" to false)).getOrThrow() startNode(customOverrides = mapOf("devMode" to false)).getOrThrow()
}.hasMessageContaining("One or more keyStores (identity or TLS) or trustStore not found.") }.hasMessageContaining("One or more keyStores (identity or TLS) or trustStore not found.")
@ -26,7 +26,7 @@ class NodeKeystoreCheckTest {
@Test(timeout=300_000) @Test(timeout=300_000)
fun `node should throw exception if cert path does not chain to the trust root`() { fun `node should throw exception if cert path does not chain to the trust root`() {
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = emptyList())) { driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = emptyList(), allowHibernateToManageAppSchema = false)) {
// Create keystores. // Create keystores.
val keystorePassword = "password" val keystorePassword = "password"
val certificatesDirectory = baseDirectory(ALICE_NAME) / "certificates" val certificatesDirectory = baseDirectory(ALICE_NAME) / "certificates"

View File

@ -0,0 +1,219 @@
package net.corda.node.amqp
import com.nhaarman.mockito_kotlin.doReturn
import com.nhaarman.mockito_kotlin.mock
import com.nhaarman.mockito_kotlin.whenever
import net.corda.core.internal.JavaVersion
import net.corda.core.internal.div
import net.corda.core.toFuture
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.seconds
import net.corda.coretesting.internal.stubs.CertificateStoreStubs
import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.config.configureWithDevSSLCertificate
import net.corda.nodeapi.internal.protonwrapper.netty.AMQPClient
import net.corda.nodeapi.internal.protonwrapper.netty.AMQPConfiguration
import net.corda.nodeapi.internal.protonwrapper.netty.init
import net.corda.nodeapi.internal.protonwrapper.netty.initialiseTrustStoreAndEnableCrlChecking
import net.corda.nodeapi.internal.protonwrapper.netty.toRevocationConfig
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.BOB_NAME
import net.corda.testing.driver.internal.incrementalPortAllocation
import org.junit.Assume.assumeFalse
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import javax.net.ssl.KeyManagerFactory
import javax.net.ssl.TrustManagerFactory
import kotlin.test.assertFalse
import kotlin.test.assertTrue
/**
* This test verifies some edge case scenarios like handshake timeouts when [AMQPClient] connected to the server
*
* In order to have control over handshake internals a simple TLS server is created which may have a configurable handshake delay.
*/
@RunWith(Parameterized::class)
class AMQPClientSslErrorsTest(@Suppress("unused") private val iteration: Int) {
companion object {
private const val MAX_MESSAGE_SIZE = 10 * 1024
private val log = contextLogger()
@JvmStatic
@Parameterized.Parameters(name = "iteration = {0}")
fun iterations(): Iterable<Array<Int>> {
// It is possible to change this value to a greater number
// to ensure that the test is not flaking when executed on CI
val repsCount = 1
return (1..repsCount).map { arrayOf(it) }
}
}
@Rule
@JvmField
val temporaryFolder = TemporaryFolder()
private val portAllocation = incrementalPortAllocation()
private lateinit var serverKeyManagerFactory: KeyManagerFactory
private lateinit var serverTrustManagerFactory: TrustManagerFactory
private lateinit var clientKeyManagerFactory: KeyManagerFactory
private lateinit var clientTrustManagerFactory: TrustManagerFactory
private lateinit var clientAmqpConfig: AMQPConfiguration
@Before
fun setup() {
setupServerCertificates()
setupClientCertificates()
}
private fun setupServerCertificates() {
val baseDirectory = temporaryFolder.root.toPath() / "server"
val certificatesDirectory = baseDirectory / "certificates"
val p2pSslConfiguration = CertificateStoreStubs.P2P.withCertificatesDirectory(certificatesDirectory)
val signingCertificateStore = CertificateStoreStubs.Signing.withCertificatesDirectory(certificatesDirectory)
val serverConfig = mock<NodeConfiguration>().also {
doReturn(baseDirectory).whenever(it).baseDirectory
doReturn(certificatesDirectory).whenever(it).certificatesDirectory
doReturn(ALICE_NAME).whenever(it).myLegalName
doReturn(p2pSslConfiguration).whenever(it).p2pSslOptions
doReturn(signingCertificateStore).whenever(it).signingCertificateStore
}
serverConfig.configureWithDevSSLCertificate()
val keyStore = serverConfig.p2pSslOptions.keyStore.get()
val serverAmqpConfig = object : AMQPConfiguration {
override val keyStore = keyStore
override val trustStore = serverConfig.p2pSslOptions.trustStore.get()
override val revocationConfig = true.toRevocationConfig()
override val maxMessageSize: Int = MAX_MESSAGE_SIZE
}
serverKeyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
serverTrustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
serverKeyManagerFactory.init(keyStore)
serverTrustManagerFactory.init(initialiseTrustStoreAndEnableCrlChecking(serverAmqpConfig.trustStore, serverAmqpConfig.revocationConfig))
}
private fun setupClientCertificates() {
val baseDirectory = temporaryFolder.root.toPath() / "client"
val certificatesDirectory = baseDirectory / "certificates"
val p2pSslConfiguration = CertificateStoreStubs.P2P.withCertificatesDirectory(certificatesDirectory)
val signingCertificateStore = CertificateStoreStubs.Signing.withCertificatesDirectory(certificatesDirectory)
val clientConfig = mock<NodeConfiguration>().also {
doReturn(baseDirectory).whenever(it).baseDirectory
doReturn(certificatesDirectory).whenever(it).certificatesDirectory
doReturn(BOB_NAME).whenever(it).myLegalName
doReturn(p2pSslConfiguration).whenever(it).p2pSslOptions
doReturn(signingCertificateStore).whenever(it).signingCertificateStore
doReturn(true).whenever(it).crlCheckSoftFail
}
clientConfig.configureWithDevSSLCertificate()
//val nodeCert = (signingCertificateStore to p2pSslConfiguration).recreateNodeCaAndTlsCertificates(nodeCrlDistPoint, tlsCrlDistPoint)
val keyStore = clientConfig.p2pSslOptions.keyStore.get()
clientAmqpConfig = object : AMQPConfiguration {
override val keyStore = keyStore
override val trustStore = clientConfig.p2pSslOptions.trustStore.get()
override val maxMessageSize: Int = MAX_MESSAGE_SIZE
override val sslHandshakeTimeout: Long = 3000
}
clientKeyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
clientTrustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
clientKeyManagerFactory.init(keyStore)
clientTrustManagerFactory.init(initialiseTrustStoreAndEnableCrlChecking(clientAmqpConfig.trustStore, clientAmqpConfig.revocationConfig))
}
@Test(timeout = 300_000)
fun trivialClientServerExchange() {
// SSL works quite differently in JDK 11 and re-work is needed
assumeFalse(JavaVersion.isVersionAtLeast(JavaVersion.Java_11))
val serverPort = portAllocation.nextPort()
val serverThread = ServerThread(serverKeyManagerFactory, serverTrustManagerFactory, serverPort).also { it.start() }
//System.setProperty("javax.net.debug", "all");
serverThread.use {
val client = NioSslClient(clientKeyManagerFactory, clientTrustManagerFactory, "localhost", serverPort)
client.connect()
client.write("Hello! I am a client!")
client.read()
client.shutdown()
val client2 = NioSslClient(clientKeyManagerFactory, clientTrustManagerFactory, "localhost", serverPort)
val client3 = NioSslClient(clientKeyManagerFactory, clientTrustManagerFactory, "localhost", serverPort)
val client4 = NioSslClient(clientKeyManagerFactory, clientTrustManagerFactory, "localhost", serverPort)
client2.connect()
client2.write("Hello! I am another client!")
client2.read()
client2.shutdown()
client3.connect()
client4.connect()
client3.write("Hello from client3!!!")
client4.write("Hello from client4!!!")
client3.read()
client4.read()
client3.shutdown()
client4.shutdown()
}
assertFalse(serverThread.isActive)
}
@Test(timeout = 300_000)
fun amqpClientServerConnect() {
// SSL works quite differently in JDK 11 and re-work is needed
assumeFalse(JavaVersion.isVersionAtLeast(JavaVersion.Java_11))
val serverPort = portAllocation.nextPort()
val serverThread = ServerThread(serverKeyManagerFactory, serverTrustManagerFactory, serverPort)
.also { it.start() }
serverThread.use {
val amqpClient = AMQPClient(listOf(NetworkHostAndPort("localhost", serverPort)), setOf(ALICE_NAME), clientAmqpConfig)
amqpClient.use {
val clientConnected = amqpClient.onConnection.toFuture()
amqpClient.start()
val clientConnect = clientConnected.get()
assertTrue(clientConnect.connected)
log.info("Confirmed connected")
}
}
assertFalse(serverThread.isActive)
}
@Test(timeout = 300_000)
fun amqpClientServerHandshakeTimeout() {
// SSL works quite differently in JDK 11 and re-work is needed
assumeFalse(JavaVersion.isVersionAtLeast(JavaVersion.Java_11))
val serverPort = portAllocation.nextPort()
val serverThread = ServerThread(serverKeyManagerFactory, serverTrustManagerFactory, serverPort, 5.seconds)
.also { it.start() }
serverThread.use {
val amqpClient = AMQPClient(listOf(NetworkHostAndPort("localhost", serverPort)), setOf(ALICE_NAME), clientAmqpConfig)
amqpClient.use {
val clientConnected = amqpClient.onConnection.toFuture()
amqpClient.start()
val clientConnect = clientConnected.get()
assertFalse(clientConnect.connected)
// Not a badCert, but a timeout during handshake
assertFalse(clientConnect.badCert)
}
}
assertFalse(serverThread.isActive)
}
}

View File

@ -34,9 +34,11 @@ import net.corda.testing.node.internal.FINANCE_CORDAPPS
import net.corda.testing.node.internal.enclosedCordapp import net.corda.testing.node.internal.enclosedCordapp
import org.junit.Test import org.junit.Test
import java.sql.SQLTransientConnectionException import java.sql.SQLTransientConnectionException
import java.util.concurrent.Semaphore import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertNull import kotlin.test.assertTrue
class FlowReloadAfterCheckpointTest { class FlowReloadAfterCheckpointTest {
@ -46,9 +48,9 @@ class FlowReloadAfterCheckpointTest {
@Test(timeout = 300_000) @Test(timeout = 300_000)
fun `flow will reload from its checkpoint after suspending when reloadCheckpointAfterSuspend is true`() { fun `flow will reload from its checkpoint after suspending when reloadCheckpointAfterSuspend is true`() {
val reloadCounts = mutableMapOf<StateMachineRunId, Int>() val reloads = ConcurrentLinkedQueue<StateMachineRunId>()
FlowStateMachineImpl.onReloadFlowFromCheckpoint = { id -> FlowStateMachineImpl.onReloadFlowFromCheckpoint = { id ->
reloadCounts.compute(id) { _, value -> value?.plus(1) ?: 1 } reloads.add(id)
} }
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = cordapps)) { driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = cordapps)) {
@ -65,16 +67,16 @@ class FlowReloadAfterCheckpointTest {
val handle = alice.rpc.startFlow(::ReloadFromCheckpointFlow, bob.nodeInfo.singleIdentity(), false, false, false) val handle = alice.rpc.startFlow(::ReloadFromCheckpointFlow, bob.nodeInfo.singleIdentity(), false, false, false)
val flowStartedByAlice = handle.id val flowStartedByAlice = handle.id
handle.returnValue.getOrThrow() handle.returnValue.getOrThrow()
assertEquals(5, reloadCounts[flowStartedByAlice]) assertEquals(5, reloads.filter { it == flowStartedByAlice }.count())
assertEquals(6, reloadCounts[ReloadFromCheckpointResponder.flowId]) assertEquals(6, reloads.filter { it == ReloadFromCheckpointResponder.flowId }.count())
} }
} }
@Test(timeout = 300_000) @Test(timeout = 300_000)
fun `flow will not reload from its checkpoint after suspending when reloadCheckpointAfterSuspend is false`() { fun `flow will not reload from its checkpoint after suspending when reloadCheckpointAfterSuspend is false`() {
val reloadCounts = mutableMapOf<StateMachineRunId, Int>() val reloads = ConcurrentLinkedQueue<StateMachineRunId>()
FlowStateMachineImpl.onReloadFlowFromCheckpoint = { id -> FlowStateMachineImpl.onReloadFlowFromCheckpoint = { id ->
reloadCounts.compute(id) { _, value -> value?.plus(1) ?: 1 } reloads.add(id)
} }
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = cordapps)) { driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = cordapps)) {
@ -89,24 +91,22 @@ class FlowReloadAfterCheckpointTest {
.getOrThrow() .getOrThrow()
val handle = alice.rpc.startFlow(::ReloadFromCheckpointFlow, bob.nodeInfo.singleIdentity(), false, false, false) val handle = alice.rpc.startFlow(::ReloadFromCheckpointFlow, bob.nodeInfo.singleIdentity(), false, false, false)
val flowStartedByAlice = handle.id
handle.returnValue.getOrThrow() handle.returnValue.getOrThrow()
assertNull(reloadCounts[flowStartedByAlice]) assertEquals(0, reloads.size)
assertNull(reloadCounts[ReloadFromCheckpointResponder.flowId])
} }
} }
@Test(timeout = 300_000) @Test(timeout = 300_000)
fun `flow will reload from its checkpoint after suspending when reloadCheckpointAfterSuspend is true and be kept for observation due to failed deserialization`() { fun `flow will reload from its checkpoint after suspending when reloadCheckpointAfterSuspend is true and be kept for observation due to failed deserialization`() {
val reloadCounts = mutableMapOf<StateMachineRunId, Int>() val reloads = ConcurrentLinkedQueue<StateMachineRunId>()
FlowStateMachineImpl.onReloadFlowFromCheckpoint = { id -> FlowStateMachineImpl.onReloadFlowFromCheckpoint = { id ->
reloadCounts.compute(id) { _, value -> value?.plus(1) ?: 1 } reloads.add(id)
} }
lateinit var flowKeptForObservation: StateMachineRunId lateinit var flowKeptForObservation: StateMachineRunId
val lock = Semaphore(0) val lock = CountDownLatch(1)
StaffedFlowHospital.onFlowKeptForOvernightObservation.add { id, _ -> StaffedFlowHospital.onFlowKeptForOvernightObservation.add { id, _ ->
flowKeptForObservation = id flowKeptForObservation = id
lock.release() lock.countDown()
} }
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = cordapps)) { driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = cordapps)) {
@ -122,18 +122,18 @@ class FlowReloadAfterCheckpointTest {
val handle = alice.rpc.startFlow(::ReloadFromCheckpointFlow, bob.nodeInfo.singleIdentity(), true, false, false) val handle = alice.rpc.startFlow(::ReloadFromCheckpointFlow, bob.nodeInfo.singleIdentity(), true, false, false)
val flowStartedByAlice = handle.id val flowStartedByAlice = handle.id
lock.acquire() lock.await()
assertEquals(flowStartedByAlice, flowKeptForObservation) assertEquals(flowStartedByAlice, flowKeptForObservation)
assertEquals(4, reloadCounts[flowStartedByAlice]) assertEquals(4, reloads.filter { it == flowStartedByAlice }.count())
assertEquals(4, reloadCounts[ReloadFromCheckpointResponder.flowId]) assertEquals(4, reloads.filter { it == ReloadFromCheckpointResponder.flowId }.count())
} }
} }
@Test(timeout = 300_000) @Test(timeout = 300_000)
fun `flow will reload from a previous checkpoint after calling suspending function and skipping the persisting the current checkpoint when reloadCheckpointAfterSuspend is true`() { fun `flow will reload from a previous checkpoint after calling suspending function and skipping the persisting the current checkpoint when reloadCheckpointAfterSuspend is true`() {
val reloadCounts = mutableMapOf<StateMachineRunId, Int>() val reloads = ConcurrentLinkedQueue<StateMachineRunId>()
FlowStateMachineImpl.onReloadFlowFromCheckpoint = { id -> FlowStateMachineImpl.onReloadFlowFromCheckpoint = { id ->
reloadCounts.compute(id) { _, value -> value?.plus(1) ?: 1 } reloads.add(id)
} }
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = cordapps)) { driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = cordapps)) {
@ -150,8 +150,8 @@ class FlowReloadAfterCheckpointTest {
val handle = alice.rpc.startFlow(::ReloadFromCheckpointFlow, bob.nodeInfo.singleIdentity(), false, false, true) val handle = alice.rpc.startFlow(::ReloadFromCheckpointFlow, bob.nodeInfo.singleIdentity(), false, false, true)
val flowStartedByAlice = handle.id val flowStartedByAlice = handle.id
handle.returnValue.getOrThrow() handle.returnValue.getOrThrow()
assertEquals(5, reloadCounts[flowStartedByAlice]) assertEquals(5, reloads.filter { it == flowStartedByAlice }.count())
assertEquals(6, reloadCounts[ReloadFromCheckpointResponder.flowId]) assertEquals(6, reloads.filter { it == ReloadFromCheckpointResponder.flowId }.count())
} }
} }
@ -189,8 +189,8 @@ class FlowReloadAfterCheckpointTest {
@Test(timeout = 300_000) @Test(timeout = 300_000)
fun `timed flow will reload from initial checkpoint after calling a suspending function when reloadCheckpointAfterSuspend is true`() { fun `timed flow will reload from initial checkpoint after calling a suspending function when reloadCheckpointAfterSuspend is true`() {
var reloadCount = 0 val reloads = ConcurrentLinkedQueue<StateMachineRunId>()
FlowStateMachineImpl.onReloadFlowFromCheckpoint = { _ -> reloadCount += 1 } FlowStateMachineImpl.onReloadFlowFromCheckpoint = { runId -> reloads.add(runId) }
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = cordapps)) { driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = cordapps)) {
val alice = startNode( val alice = startNode(
@ -199,14 +199,14 @@ class FlowReloadAfterCheckpointTest {
).getOrThrow() ).getOrThrow()
alice.rpc.startFlow(::MyTimedFlow).returnValue.getOrThrow() alice.rpc.startFlow(::MyTimedFlow).returnValue.getOrThrow()
assertEquals(5, reloadCount) assertEquals(5, reloads.size)
} }
} }
@Test(timeout = 300_000) @Test(timeout = 300_000)
fun `flow will correctly retry after an error when reloadCheckpointAfterSuspend is true`() { fun `flow will correctly retry after an error when reloadCheckpointAfterSuspend is true`() {
var reloadCount = 0 val reloads = ConcurrentLinkedQueue<StateMachineRunId>()
FlowStateMachineImpl.onReloadFlowFromCheckpoint = { _ -> reloadCount += 1 } FlowStateMachineImpl.onReloadFlowFromCheckpoint = { runId -> reloads.add(runId) }
var timesDischarged = 0 var timesDischarged = 0
StaffedFlowHospital.onFlowDischarged.add { _, _ -> timesDischarged += 1 } StaffedFlowHospital.onFlowDischarged.add { _, _ -> timesDischarged += 1 }
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = cordapps)) { driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = cordapps)) {
@ -217,15 +217,21 @@ class FlowReloadAfterCheckpointTest {
).getOrThrow() ).getOrThrow()
alice.rpc.startFlow(::TransientConnectionFailureFlow).returnValue.getOrThrow() alice.rpc.startFlow(::TransientConnectionFailureFlow).returnValue.getOrThrow()
assertEquals(5, reloadCount) assertEquals(5, reloads.size)
assertEquals(3, timesDischarged) assertEquals(3, timesDischarged)
} }
} }
@Test(timeout = 300_000) @Test(timeout = 300_000)
fun `flow continues reloading from checkpoints after node restart when reloadCheckpointAfterSuspend is true`() { fun `flow continues reloading from checkpoints after node restart when reloadCheckpointAfterSuspend is true`() {
var reloadCount = 0 val reloads = ConcurrentLinkedQueue<StateMachineRunId>()
FlowStateMachineImpl.onReloadFlowFromCheckpoint = { _ -> reloadCount += 1 } val firstLatch = CountDownLatch(2)
val secondLatch = CountDownLatch(5)
FlowStateMachineImpl.onReloadFlowFromCheckpoint = { runId ->
reloads.add(runId)
firstLatch.countDown()
secondLatch.countDown()
}
driver( driver(
DriverParameters( DriverParameters(
inMemoryDB = false, inMemoryDB = false,
@ -241,25 +247,31 @@ class FlowReloadAfterCheckpointTest {
).getOrThrow() ).getOrThrow()
alice.rpc.startFlow(::MyHospitalizingFlow) alice.rpc.startFlow(::MyHospitalizingFlow)
Thread.sleep(10.seconds.toMillis()) assertTrue { firstLatch.await(10, TimeUnit.SECONDS) }
alice.stop() alice.stop()
assertEquals(2, reloads.size)
// Set up a new latch
startNode( startNode(
providedName = ALICE_NAME, providedName = ALICE_NAME,
customOverrides = mapOf(NodeConfiguration::reloadCheckpointAfterSuspend.name to true) customOverrides = mapOf(NodeConfiguration::reloadCheckpointAfterSuspend.name to true)
).getOrThrow() ).getOrThrow()
Thread.sleep(20.seconds.toMillis()) assertTrue { secondLatch.await(20, TimeUnit.SECONDS) }
assertEquals(5, reloads.size)
assertEquals(5, reloadCount)
} }
} }
@Test(timeout = 300_000) @Test(timeout = 300_000)
fun `idempotent flow continues reloading from checkpoints after node restart when reloadCheckpointAfterSuspend is true`() { fun `idempotent flow continues reloading from checkpoints after node restart when reloadCheckpointAfterSuspend is true`() {
var reloadCount = 0 // restarts completely from the beginning and forgets the in-memory reload count therefore
FlowStateMachineImpl.onReloadFlowFromCheckpoint = { _ -> reloadCount += 1 } // it reloads an extra 2 times for checkpoints it had already reloaded before the node shutdown
val reloadsExpected = CountDownLatch(7)
val reloads = ConcurrentLinkedQueue<StateMachineRunId>()
FlowStateMachineImpl.onReloadFlowFromCheckpoint = { runId ->
reloads.add(runId)
reloadsExpected.countDown()
}
driver( driver(
DriverParameters( DriverParameters(
inMemoryDB = false, inMemoryDB = false,
@ -284,19 +296,18 @@ class FlowReloadAfterCheckpointTest {
customOverrides = mapOf(NodeConfiguration::reloadCheckpointAfterSuspend.name to true) customOverrides = mapOf(NodeConfiguration::reloadCheckpointAfterSuspend.name to true)
).getOrThrow() ).getOrThrow()
Thread.sleep(20.seconds.toMillis())
// restarts completely from the beginning and forgets the in-memory reload count therefore // restarts completely from the beginning and forgets the in-memory reload count therefore
// it reloads an extra 2 times for checkpoints it had already reloaded before the node shutdown // it reloads an extra 2 times for checkpoints it had already reloaded before the node shutdown
assertEquals(7, reloadCount) assertTrue { reloadsExpected.await(20, TimeUnit.SECONDS) }
assertEquals(7, reloads.size)
} }
} }
@Test(timeout = 300_000) @Test(timeout = 300_000)
fun `more complicated flow will reload from its checkpoint after suspending when reloadCheckpointAfterSuspend is true`() { fun `more complicated flow will reload from its checkpoint after suspending when reloadCheckpointAfterSuspend is true`() {
val reloadCounts = mutableMapOf<StateMachineRunId, Int>() val reloads = ConcurrentLinkedQueue<StateMachineRunId>()
FlowStateMachineImpl.onReloadFlowFromCheckpoint = { id -> FlowStateMachineImpl.onReloadFlowFromCheckpoint = { id ->
reloadCounts.compute(id) { _, value -> value?.plus(1) ?: 1 } reloads.add(id)
} }
driver(DriverParameters(startNodesInProcess = true, cordappsForAllNodes = FINANCE_CORDAPPS)) { driver(DriverParameters(startNodesInProcess = true, cordappsForAllNodes = FINANCE_CORDAPPS)) {
@ -325,8 +336,8 @@ class FlowReloadAfterCheckpointTest {
.toSet() .toSet()
.single() .single()
Thread.sleep(10.seconds.toMillis()) Thread.sleep(10.seconds.toMillis())
assertEquals(7, reloadCounts[flowStartedByAlice]) assertEquals(7, reloads.filter { it == flowStartedByAlice }.size)
assertEquals(6, reloadCounts[flowStartedByBob]) assertEquals(6, reloads.filter { it == flowStartedByBob }.size)
} }
} }

View File

@ -36,6 +36,7 @@ import net.corda.testing.driver.OutOfProcess
import net.corda.testing.driver.driver import net.corda.testing.driver.driver
import net.corda.testing.node.internal.FINANCE_CORDAPPS import net.corda.testing.node.internal.FINANCE_CORDAPPS
import org.assertj.core.api.Assertions import org.assertj.core.api.Assertions
import org.junit.Ignore
import org.junit.Test import org.junit.Test
import java.time.Duration import java.time.Duration
import java.util.concurrent.Semaphore import java.util.concurrent.Semaphore
@ -198,6 +199,7 @@ class KillFlowTest {
} }
} }
@Ignore("CORDA-3948: Disabled pending availability of engineers to diagnose")
@Test(timeout = 300_000) @Test(timeout = 300_000)
fun `a killed flow will propagate the killed error to counter parties if it was suspended`() { fun `a killed flow will propagate the killed error to counter parties if it was suspended`() {
driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) {

View File

@ -2,32 +2,21 @@ package net.corda.node.persistence
import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.getOrThrow
import net.corda.node.flows.isQuasarAgentSpecified import net.corda.node.flows.isQuasarAgentSpecified
import net.corda.nodeapi.internal.persistence.DatabaseIncompatibleException import net.corda.node.internal.ConfigurationException
import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.DriverParameters
import net.corda.testing.driver.NodeParameters import net.corda.testing.driver.NodeParameters
import net.corda.testing.driver.driver import net.corda.testing.driver.driver
import org.junit.Test import org.junit.Test
import kotlin.test.assertFailsWith import kotlin.test.assertFailsWith
import kotlin.test.assertNotNull
class DbSchemaInitialisationTest { class DbSchemaInitialisationTest {
@Test(timeout = 300_000) @Test(timeout = 300_000)
fun `database is initialised`() { fun `database initialisation not allowed in config`() {
driver(DriverParameters(startNodesInProcess = isQuasarAgentSpecified(), cordappsForAllNodes = emptyList())) { driver(DriverParameters(startNodesInProcess = isQuasarAgentSpecified(), cordappsForAllNodes = emptyList())) {
val nodeHandle = { assertFailsWith(ConfigurationException::class) {
startNode(NodeParameters(customOverrides = mapOf("database.initialiseSchema" to "true"))).getOrThrow()
}()
assertNotNull(nodeHandle)
}
}
@Test(timeout=300_000)
fun `database is not initialised`() {
driver(DriverParameters(startNodesInProcess = isQuasarAgentSpecified(), cordappsForAllNodes = emptyList())) {
assertFailsWith(DatabaseIncompatibleException::class) {
startNode(NodeParameters(customOverrides = mapOf("database.initialiseSchema" to "false"))).getOrThrow() startNode(NodeParameters(customOverrides = mapOf("database.initialiseSchema" to "false"))).getOrThrow()
} }
} }
} }
} }

View File

@ -1,8 +1,10 @@
package net.corda.node.services.network package net.corda.node.services.network
import net.corda.core.crypto.random63BitValue import net.corda.core.crypto.random63BitValue
import net.corda.core.identity.Party
import net.corda.core.internal.* import net.corda.core.internal.*
import net.corda.core.messaging.ParametersUpdateInfo import net.corda.core.messaging.ParametersUpdateInfo
import net.corda.core.node.NetworkParameters
import net.corda.core.node.NodeInfo import net.corda.core.node.NodeInfo
import net.corda.core.serialization.serialize import net.corda.core.serialization.serialize
import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.getOrThrow
@ -11,6 +13,7 @@ import net.corda.nodeapi.internal.SignedNodeInfo
import net.corda.nodeapi.internal.network.NETWORK_PARAMS_FILE_NAME import net.corda.nodeapi.internal.network.NETWORK_PARAMS_FILE_NAME
import net.corda.nodeapi.internal.network.NETWORK_PARAMS_UPDATE_FILE_NAME import net.corda.nodeapi.internal.network.NETWORK_PARAMS_UPDATE_FILE_NAME
import net.corda.nodeapi.internal.network.SignedNetworkParameters import net.corda.nodeapi.internal.network.SignedNetworkParameters
import net.corda.testing.common.internal.addNotary
import net.corda.testing.common.internal.eventually import net.corda.testing.common.internal.eventually
import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.core.* import net.corda.testing.core.*
@ -74,7 +77,6 @@ class NetworkMapTest(var initFunc: (URL, NetworkMapServer) -> CompatibilityZoneP
) )
} }
@Before @Before
fun start() { fun start() {
networkMapServer = NetworkMapServer(cacheTimeout, portAllocation.nextHostAndPort()) networkMapServer = NetworkMapServer(cacheTimeout, portAllocation.nextHostAndPort())
@ -92,7 +94,8 @@ class NetworkMapTest(var initFunc: (URL, NetworkMapServer) -> CompatibilityZoneP
internalDriver( internalDriver(
portAllocation = portAllocation, portAllocation = portAllocation,
compatibilityZone = compatibilityZone, compatibilityZone = compatibilityZone,
notarySpecs = emptyList() notarySpecs = emptyList(),
allowHibernateToManageAppSchema = false
) { ) {
val alice = startNode(providedName = ALICE_NAME, devMode = false).getOrThrow() as NodeHandleInternal val alice = startNode(providedName = ALICE_NAME, devMode = false).getOrThrow() as NodeHandleInternal
val nextParams = networkMapServer.networkParameters.copy( val nextParams = networkMapServer.networkParameters.copy(
@ -141,22 +144,125 @@ class NetworkMapTest(var initFunc: (URL, NetworkMapServer) -> CompatibilityZoneP
} }
} }
@Test(timeout = 300_000)
fun `Can hotload parameters if the notary changes`() {
internalDriver(
portAllocation = portAllocation,
compatibilityZone = compatibilityZone,
notarySpecs = emptyList(),
allowHibernateToManageAppSchema = false
) {
val notary: Party = TestIdentity.fresh("test notary").party
val oldParams = networkMapServer.networkParameters
val paramsWithNewNotary = oldParams.copy(
epoch = 3,
modifiedTime = Instant.ofEpochMilli(random63BitValue())).addNotary(notary)
startNodeAndRunFlagDay(paramsWithNewNotary).use { alice ->
eventually { assertEquals(paramsWithNewNotary, alice.rpc.networkParameters) }
}
}
}
@Test(timeout = 300_000)
fun `If only the notary changes but parameters were not accepted, the node will still shut down on the flag day`() {
internalDriver(
portAllocation = portAllocation,
compatibilityZone = compatibilityZone,
notarySpecs = emptyList(),
allowHibernateToManageAppSchema = false
) {
val notary: Party = TestIdentity.fresh("test notary").party
val oldParams = networkMapServer.networkParameters
val paramsWithNewNotary = oldParams.copy(
epoch = 3,
modifiedTime = Instant.ofEpochMilli(random63BitValue())).addNotary(notary)
val alice = startNode(providedName = ALICE_NAME, devMode = false).getOrThrow() as NodeHandleInternal
networkMapServer.scheduleParametersUpdate(paramsWithNewNotary, "Next parameters", Instant.ofEpochMilli(random63BitValue()))
// Wait for network map client to poll for the next update.
Thread.sleep(cacheTimeout.toMillis() * 2)
networkMapServer.advertiseNewParameters()
eventually { assertThatThrownBy { alice.rpc.networkParameters }.hasMessageContaining("Connection failure detected") }
}
}
@Test(timeout = 300_000)
fun `Can not hotload parameters if non-hotloadable parameter changes and the node will shut down`() {
internalDriver(
portAllocation = portAllocation,
compatibilityZone = compatibilityZone,
notarySpecs = emptyList(),
allowHibernateToManageAppSchema = false
) {
val oldParams = networkMapServer.networkParameters
val paramsWithUpdatedMaxMessageSize = oldParams.copy(
epoch = 3,
modifiedTime = Instant.ofEpochMilli(random63BitValue()),
maxMessageSize = oldParams.maxMessageSize + 1)
startNodeAndRunFlagDay(paramsWithUpdatedMaxMessageSize).use { alice ->
eventually { assertThatThrownBy { alice.rpc.networkParameters }.hasMessageContaining("Connection failure detected") }
}
}
}
@Test(timeout = 300_000)
fun `Can not hotload parameters if notary and a non-hotloadable parameter changes and the node will shut down`() {
internalDriver(
portAllocation = portAllocation,
compatibilityZone = compatibilityZone,
notarySpecs = emptyList(),
allowHibernateToManageAppSchema = false
) {
val oldParams = networkMapServer.networkParameters
val notary: Party = TestIdentity.fresh("test notary").party
val paramsWithUpdatedMaxMessageSizeAndNotary = oldParams.copy(
epoch = 3,
modifiedTime = Instant.ofEpochMilli(random63BitValue()),
maxMessageSize = oldParams.maxMessageSize + 1).addNotary(notary)
startNodeAndRunFlagDay(paramsWithUpdatedMaxMessageSizeAndNotary).use { alice ->
eventually { assertThatThrownBy { alice.rpc.networkParameters }.hasMessageContaining("Connection failure detected") }
}
}
}
private fun DriverDSLImpl.startNodeAndRunFlagDay(newParams: NetworkParameters): NodeHandleInternal {
val alice = startNode(providedName = ALICE_NAME, devMode = false).getOrThrow() as NodeHandleInternal
val nextHash = newParams.serialize().hash
networkMapServer.scheduleParametersUpdate(newParams, "Next parameters", Instant.ofEpochMilli(random63BitValue()))
// Wait for network map client to poll for the next update.
Thread.sleep(cacheTimeout.toMillis() * 2)
alice.rpc.acceptNewNetworkParameters(nextHash)
assertEquals(nextHash, networkMapServer.latestParametersAccepted(alice.nodeInfo.legalIdentities.first().owningKey))
assertEquals(networkMapServer.networkParameters, alice.rpc.networkParameters)
networkMapServer.advertiseNewParameters()
return alice
}
@Test(timeout=300_000) @Test(timeout=300_000)
fun `nodes process additions and removals from the network map correctly (and also download the network parameters)`() { fun `nodes process additions and removals from the network map correctly (and also download the network parameters)`() {
internalDriver( internalDriver(
portAllocation = portAllocation, portAllocation = portAllocation,
compatibilityZone = compatibilityZone, compatibilityZone = compatibilityZone,
notarySpecs = emptyList() notarySpecs = emptyList(),
allowHibernateToManageAppSchema = false
) { ) {
val aliceNode = startNode(providedName = ALICE_NAME, devMode = false).getOrThrow() startNode(providedName = ALICE_NAME, devMode = false).getOrThrow().use { aliceNode ->
assertDownloadedNetworkParameters(aliceNode) assertDownloadedNetworkParameters(aliceNode)
aliceNode.onlySees(aliceNode.nodeInfo) aliceNode.onlySees(aliceNode.nodeInfo)
val bobNode = startNode(providedName = BOB_NAME, devMode = false).getOrThrow()
// Wait for network map client to poll for the next update. // Wait for network map client to poll for the next update.
Thread.sleep(cacheTimeout.toMillis() * 2) Thread.sleep(cacheTimeout.toMillis() * 2)
startNode(providedName = BOB_NAME, devMode = false).getOrThrow().use { bobNode ->
bobNode.onlySees(aliceNode.nodeInfo, bobNode.nodeInfo) bobNode.onlySees(aliceNode.nodeInfo, bobNode.nodeInfo)
aliceNode.onlySees(aliceNode.nodeInfo, bobNode.nodeInfo) aliceNode.onlySees(aliceNode.nodeInfo, bobNode.nodeInfo)
@ -168,6 +274,8 @@ class NetworkMapTest(var initFunc: (URL, NetworkMapServer) -> CompatibilityZoneP
bobNode.onlySees(bobNode.nodeInfo) bobNode.onlySees(bobNode.nodeInfo)
} }
} }
}
}
@Test(timeout=300_000) @Test(timeout=300_000)
fun `test node heartbeat`() { fun `test node heartbeat`() {
@ -175,9 +283,10 @@ class NetworkMapTest(var initFunc: (URL, NetworkMapServer) -> CompatibilityZoneP
portAllocation = portAllocation, portAllocation = portAllocation,
compatibilityZone = compatibilityZone, compatibilityZone = compatibilityZone,
notarySpecs = emptyList(), notarySpecs = emptyList(),
systemProperties = mapOf("net.corda.node.internal.nodeinfo.publish.interval" to 1.seconds.toString()) systemProperties = mapOf("net.corda.node.internal.nodeinfo.publish.interval" to 1.seconds.toString()),
allowHibernateToManageAppSchema = false
) { ) {
val aliceNode = startNode(providedName = ALICE_NAME, devMode = false).getOrThrow() startNode(providedName = ALICE_NAME, devMode = false).getOrThrow().use { aliceNode ->
val aliceNodeInfo = aliceNode.nodeInfo.serialize().hash val aliceNodeInfo = aliceNode.nodeInfo.serialize().hash
assertThat(networkMapServer.networkMapHashes()).contains(aliceNodeInfo) assertThat(networkMapServer.networkMapHashes()).contains(aliceNodeInfo)
networkMapServer.removeNodeInfo(aliceNode.nodeInfo) networkMapServer.removeNodeInfo(aliceNode.nodeInfo)
@ -197,6 +306,7 @@ class NetworkMapTest(var initFunc: (URL, NetworkMapServer) -> CompatibilityZoneP
assertThat(networkMapServer.networkMapHashes()).contains(aliceNodeInfo) assertThat(networkMapServer.networkMapHashes()).contains(aliceNodeInfo)
} }
} }
}
private fun assertDownloadedNetworkParameters(node: NodeHandle) { private fun assertDownloadedNetworkParameters(node: NodeHandle) {
val networkParameters = (node.baseDirectory / NETWORK_PARAMS_FILE_NAME) val networkParameters = (node.baseDirectory / NETWORK_PARAMS_FILE_NAME)

View File

@ -58,7 +58,7 @@ class RpcExceptionHandlingTest {
} }
} }
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = listOf(enclosedCordapp()))) { driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = listOf(enclosedCordapp()), allowHibernateToManageAppSchema = false)) {
val (devModeNode, node) = listOf(startNode(params, BOB_NAME), val (devModeNode, node) = listOf(startNode(params, BOB_NAME),
startNode(ALICE_NAME, devMode = false, parameters = params)) startNode(ALICE_NAME, devMode = false, parameters = params))
.transpose() .transpose()
@ -79,7 +79,7 @@ class RpcExceptionHandlingTest {
rpc.startFlow(::FlowExceptionFlow, expectedMessage, expectedErrorId).returnValue.getOrThrow() rpc.startFlow(::FlowExceptionFlow, expectedMessage, expectedErrorId).returnValue.getOrThrow()
} }
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = listOf(enclosedCordapp()))) { driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = listOf(enclosedCordapp()), allowHibernateToManageAppSchema = false)) {
val (devModeNode, node) = listOf(startNode(params, BOB_NAME), val (devModeNode, node) = listOf(startNode(params, BOB_NAME),
startNode(ALICE_NAME, devMode = false, parameters = params)) startNode(ALICE_NAME, devMode = false, parameters = params))
.transpose() .transpose()
@ -115,7 +115,7 @@ class RpcExceptionHandlingTest {
nodeA.rpc.startFlow(::InitFlow, nodeB.nodeInfo.singleIdentity()).returnValue.getOrThrow() nodeA.rpc.startFlow(::InitFlow, nodeB.nodeInfo.singleIdentity()).returnValue.getOrThrow()
} }
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = listOf(enclosedCordapp()))) { driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = listOf(enclosedCordapp()), allowHibernateToManageAppSchema = false)) {
assertThatThrownBy { scenario(ALICE_NAME, BOB_NAME,true) }.isInstanceOfSatisfying(CordaRuntimeException::class.java) { exception -> assertThatThrownBy { scenario(ALICE_NAME, BOB_NAME,true) }.isInstanceOfSatisfying(CordaRuntimeException::class.java) { exception ->

View File

@ -33,13 +33,19 @@ class FlowVersioningTest : NodeBasedTest() {
private class PretendInitiatingCoreFlow(val initiatedParty: Party) : FlowLogic<Pair<Int, Int>>() { private class PretendInitiatingCoreFlow(val initiatedParty: Party) : FlowLogic<Pair<Int, Int>>() {
@Suspendable @Suspendable
override fun call(): Pair<Int, Int> { override fun call(): Pair<Int, Int> {
// Execute receive() outside of the Pair constructor to avoid Kotlin/Quasar instrumentation bug.
val session = initiateFlow(initiatedParty) val session = initiateFlow(initiatedParty)
return try {
// Get counterparty flow info before we receive Alice's data, to ensure the flow is still open
val bobPlatformVersionAccordingToAlice = session.getCounterpartyFlowInfo().flowVersion
// Execute receive() outside of the Pair constructor to avoid Kotlin/Quasar instrumentation bug.
val alicePlatformVersionAccordingToBob = session.receive<Int>().unwrap { it } val alicePlatformVersionAccordingToBob = session.receive<Int>().unwrap { it }
return Pair( Pair(
alicePlatformVersionAccordingToBob, alicePlatformVersionAccordingToBob,
session.getCounterpartyFlowInfo().flowVersion bobPlatformVersionAccordingToAlice
) )
} finally {
session.close()
}
} }
} }

View File

@ -42,7 +42,6 @@ class P2PMessagingTest {
private fun startDriverWithDistributedService(dsl: DriverDSL.(List<InProcess>) -> Unit) { private fun startDriverWithDistributedService(dsl: DriverDSL.(List<InProcess>) -> Unit) {
driver(DriverParameters( driver(DriverParameters(
startNodesInProcess = true, startNodesInProcess = true,
extraCordappPackagesToScan = listOf("net.corda.notary.raft"),
notarySpecs = listOf(NotarySpec(DISTRIBUTED_SERVICE_NAME, cluster = ClusterSpec.Raft(clusterSize = 2))) notarySpecs = listOf(NotarySpec(DISTRIBUTED_SERVICE_NAME, cluster = ClusterSpec.Raft(clusterSize = 2)))
)) { )) {
dsl(defaultNotaryHandle.nodeHandles.getOrThrow().map { (it as InProcess) }) dsl(defaultNotaryHandle.nodeHandles.getOrThrow().map { (it as InProcess) })

View File

@ -48,6 +48,14 @@ open class SharedNodeCmdLineOptions {
) )
var devMode: Boolean? = null var devMode: Boolean? = null
@Option(
names = ["--allow-hibernate-to-manage-app-schema"],
description = ["Allows hibernate to create/modify app schema for CorDapps based on their mapped schema.",
"Use this for rapid app development or for compatibility with pre-4.6 CorDapps.",
"Only available in dev mode."]
)
var allowHibernateToManageAppSchema: Boolean = false
open fun parseConfiguration(configuration: Config): Valid<NodeConfiguration> { open fun parseConfiguration(configuration: Config): Valid<NodeConfiguration> {
val option = Configuration.Options(strict = unknownConfigKeysPolicy == UnknownConfigKeysPolicy.FAIL) val option = Configuration.Options(strict = unknownConfigKeysPolicy == UnknownConfigKeysPolicy.FAIL)
return configuration.parseAsNodeConfiguration(option) return configuration.parseAsNodeConfiguration(option)

View File

@ -109,6 +109,8 @@ import net.corda.node.services.messaging.DeduplicationHandler
import net.corda.node.services.messaging.MessagingService import net.corda.node.services.messaging.MessagingService
import net.corda.node.services.network.NetworkMapClient import net.corda.node.services.network.NetworkMapClient
import net.corda.node.services.network.NetworkMapUpdater import net.corda.node.services.network.NetworkMapUpdater
import net.corda.node.services.network.NetworkParameterUpdateListener
import net.corda.node.services.network.NetworkParametersHotloader
import net.corda.node.services.network.NodeInfoWatcher import net.corda.node.services.network.NodeInfoWatcher
import net.corda.node.services.network.PersistentNetworkMapCache import net.corda.node.services.network.PersistentNetworkMapCache
import net.corda.node.services.persistence.AbstractPartyDescriptor import net.corda.node.services.persistence.AbstractPartyDescriptor
@ -176,7 +178,6 @@ import org.slf4j.Logger
import rx.Scheduler import rx.Scheduler
import java.io.IOException import java.io.IOException
import java.lang.reflect.InvocationTargetException import java.lang.reflect.InvocationTargetException
import java.nio.file.Path
import java.security.KeyPair import java.security.KeyPair
import java.security.KeyStoreException import java.security.KeyStoreException
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
@ -185,7 +186,7 @@ import java.sql.Savepoint
import java.time.Clock import java.time.Clock
import java.time.Duration import java.time.Duration
import java.time.format.DateTimeParseException import java.time.format.DateTimeParseException
import java.util.Properties import java.util.*
import java.util.concurrent.ExecutorService import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors import java.util.concurrent.Executors
import java.util.concurrent.LinkedBlockingQueue import java.util.concurrent.LinkedBlockingQueue
@ -195,6 +196,8 @@ import java.util.concurrent.TimeUnit.MINUTES
import java.util.concurrent.TimeUnit.SECONDS import java.util.concurrent.TimeUnit.SECONDS
import java.util.function.Consumer import java.util.function.Consumer
import javax.persistence.EntityManager import javax.persistence.EntityManager
import javax.sql.DataSource
import kotlin.collections.ArrayList
/** /**
* A base node implementation that can be customised either for production (with real implementations that do real * A base node implementation that can be customised either for production (with real implementations that do real
@ -212,9 +215,12 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
val serverThread: AffinityExecutor.ServiceAffinityExecutor, val serverThread: AffinityExecutor.ServiceAffinityExecutor,
val busyNodeLatch: ReusableLatch = ReusableLatch(), val busyNodeLatch: ReusableLatch = ReusableLatch(),
djvmBootstrapSource: ApiSource = EmptyApi, djvmBootstrapSource: ApiSource = EmptyApi,
djvmCordaSource: UserSource? = null) : SingletonSerializeAsToken() { djvmCordaSource: UserSource? = null,
protected val allowHibernateToManageAppSchema: Boolean = false,
private val allowAppSchemaUpgradeWithCheckpoints: Boolean = false) : SingletonSerializeAsToken() {
protected abstract val log: Logger protected abstract val log: Logger
@Suppress("LeakingThis") @Suppress("LeakingThis")
private var tokenizableServices: MutableList<SerializeAsToken>? = mutableListOf(platformClock, this) private var tokenizableServices: MutableList<SerializeAsToken>? = mutableListOf(platformClock, this)
@ -224,6 +230,11 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
protected val runOnStop = ArrayList<() -> Any?>() protected val runOnStop = ArrayList<() -> Any?>()
protected open val runMigrationScripts: Boolean = configuredDbIsInMemory()
// if the configured DB is in memory, we will need to run db migrations, as the db does not persist between runs.
private fun configuredDbIsInMemory() = configuration.dataSourceProperties.getProperty("dataSource.url").startsWith("jdbc:h2:mem:")
init { init {
(serverThread as? ExecutorService)?.let { (serverThread as? ExecutorService)?.let {
runOnStop += { runOnStop += {
@ -235,6 +246,12 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
} }
quasarExcludePackages(configuration) quasarExcludePackages(configuration)
if (allowHibernateToManageAppSchema && !configuration.devMode) {
throw ConfigurationException("Hibernate can only be used to manage app schema in development while using dev mode. " +
"Please remove the --allow-hibernate-to-manage-app-schema command line flag and provide schema migration scripts for your CorDapps."
)
}
} }
private val notaryLoader = configuration.notary?.let { private val notaryLoader = configuration.notary?.let {
@ -250,7 +267,8 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
schemaService, schemaService,
configuration.dataSourceProperties, configuration.dataSourceProperties,
cacheFactory, cacheFactory,
cordappLoader.appClassLoader) cordappLoader.appClassLoader,
allowHibernateToManageAppSchema)
private val transactionSupport = CordaTransactionSupportImpl(database) private val transactionSupport = CordaTransactionSupportImpl(database)
@ -461,6 +479,54 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
} }
} }
open fun runDatabaseMigrationScripts(
updateCoreSchemas: Boolean,
updateAppSchemas: Boolean,
updateAppSchemasWithCheckpoints: Boolean
) {
check(started == null) { "Node has already been started" }
Node.printBasicNodeInfo("Running database schema migration scripts ...")
val props = configuration.dataSourceProperties
if (props.isEmpty) throw DatabaseConfigurationException("There must be a database configured.")
database.startHikariPool(props, metricRegistry) { dataSource, haveCheckpoints ->
SchemaMigration(dataSource, cordappLoader, configuration.baseDirectory, configuration.myLegalName)
.checkOrUpdate(schemaService.internalSchemas, updateCoreSchemas, haveCheckpoints, true)
.checkOrUpdate(schemaService.appSchemas, updateAppSchemas, !updateAppSchemasWithCheckpoints && haveCheckpoints, false)
}
// Now log the vendor string as this will also cause a connection to be tested eagerly.
logVendorString(database, log)
if (allowHibernateToManageAppSchema) {
Node.printBasicNodeInfo("Initialising CorDapps to get schemas created by hibernate")
val trustRoot = initKeyStores()
networkMapClient?.start(trustRoot)
val (netParams, signedNetParams) = NetworkParametersReader(trustRoot, networkMapClient, configuration.baseDirectory).read()
log.info("Loaded network parameters: $netParams")
check(netParams.minimumPlatformVersion <= versionInfo.platformVersion) {
"Node's platform version is lower than network's required minimumPlatformVersion"
}
networkMapCache.start(netParams.notaries)
database.transaction {
networkParametersStorage.setCurrentParameters(signedNetParams, trustRoot)
cordappProvider.start()
}
}
Node.printBasicNodeInfo("Database migration done.")
}
fun runSchemaSync() {
check(started == null) { "Node has already been started" }
Node.printBasicNodeInfo("Synchronising CorDapp schemas to the changelog ...")
val hikariProperties = configuration.dataSourceProperties
if (hikariProperties.isEmpty) throw DatabaseConfigurationException("There must be a database configured.")
val dataSource = DataSourceFactory.createDataSource(hikariProperties, metricRegistry = metricRegistry)
SchemaMigration(dataSource, cordappLoader, configuration.baseDirectory, configuration.myLegalName)
.synchroniseSchemas(schemaService.appSchemas, false)
Node.printBasicNodeInfo("CorDapp schemas synchronised")
}
@Suppress("ComplexMethod")
open fun start(): S { open fun start(): S {
check(started == null) { "Node has already been started" } check(started == null) { "Node has already been started" }
@ -486,7 +552,8 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
startShell() startShell()
networkMapClient?.start(trustRoot) networkMapClient?.start(trustRoot)
val (netParams, signedNetParams) = NetworkParametersReader(trustRoot, networkMapClient, configuration.baseDirectory).read() val networkParametersReader = NetworkParametersReader(trustRoot, networkMapClient, configuration.baseDirectory)
val (netParams, signedNetParams) = networkParametersReader.read()
log.info("Loaded network parameters: $netParams") log.info("Loaded network parameters: $netParams")
check(netParams.minimumPlatformVersion <= versionInfo.platformVersion) { check(netParams.minimumPlatformVersion <= versionInfo.platformVersion) {
"Node's platform version is lower than network's required minimumPlatformVersion" "Node's platform version is lower than network's required minimumPlatformVersion"
@ -507,13 +574,27 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
val (nodeInfo, signedNodeInfo) = nodeInfoAndSigned val (nodeInfo, signedNodeInfo) = nodeInfoAndSigned
identityService.ourNames = nodeInfo.legalIdentities.map { it.name }.toSet() identityService.ourNames = nodeInfo.legalIdentities.map { it.name }.toSet()
services.start(nodeInfo, netParams) services.start(nodeInfo, netParams)
val networkParametersHotloader = if (networkMapClient == null) {
null
} else {
NetworkParametersHotloader(networkMapClient, trustRoot, netParams, networkParametersReader, networkParametersStorage).also {
it.addNotaryUpdateListener(networkMapCache)
it.addNotaryUpdateListener(identityService)
it.addNetworkParametersChangedListeners(services)
it.addNetworkParametersChangedListeners(networkMapUpdater)
}
}
networkMapUpdater.start( networkMapUpdater.start(
trustRoot, trustRoot,
signedNetParams.raw.hash, signedNetParams.raw.hash,
signedNodeInfo, signedNodeInfo,
netParams, netParams,
keyManagementService, keyManagementService,
configuration.networkParameterAcceptanceSettings!!) configuration.networkParameterAcceptanceSettings!!,
networkParametersHotloader)
try { try {
startMessagingService(rpcOps, nodeInfo, myNotaryIdentity, netParams) startMessagingService(rpcOps, nodeInfo, myNotaryIdentity, netParams)
} catch (e: Exception) { } catch (e: Exception) {
@ -956,7 +1037,12 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
protected open fun startDatabase() { protected open fun startDatabase() {
val props = configuration.dataSourceProperties val props = configuration.dataSourceProperties
if (props.isEmpty) throw DatabaseConfigurationException("There must be a database configured.") if (props.isEmpty) throw DatabaseConfigurationException("There must be a database configured.")
database.startHikariPool(props, configuration.database, schemaService.internalSchemas(), metricRegistry, this.cordappLoader, configuration.baseDirectory, configuration.myLegalName) database.startHikariPool(props, metricRegistry) { dataSource, haveCheckpoints ->
SchemaMigration(dataSource, cordappLoader, configuration.baseDirectory, configuration.myLegalName)
.checkOrUpdate(schemaService.internalSchemas, runMigrationScripts, haveCheckpoints, true)
.checkOrUpdate(schemaService.appSchemas, runMigrationScripts, haveCheckpoints && !allowAppSchemaUpgradeWithCheckpoints, false)
}
// Now log the vendor string as this will also cause a connection to be tested eagerly. // Now log the vendor string as this will also cause a connection to be tested eagerly.
logVendorString(database, log) logVendorString(database, log)
} }
@ -1153,7 +1239,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
} }
} }
inner class ServiceHubInternalImpl : SingletonSerializeAsToken(), ServiceHubInternal, ServicesForResolution by servicesForResolution { inner class ServiceHubInternalImpl : SingletonSerializeAsToken(), ServiceHubInternal, ServicesForResolution by servicesForResolution, NetworkParameterUpdateListener {
override val rpcFlows = ArrayList<Class<out FlowLogic<*>>>() override val rpcFlows = ArrayList<Class<out FlowLogic<*>>>()
override val stateMachineRecordedTransactionMapping = DBTransactionMappingStorage(database) override val stateMachineRecordedTransactionMapping = DBTransactionMappingStorage(database)
override val identityService: IdentityService get() = this@AbstractNode.identityService override val identityService: IdentityService get() = this@AbstractNode.identityService
@ -1186,6 +1272,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
override val attachmentsClassLoaderCache: AttachmentsClassLoaderCache get() = this@AbstractNode.attachmentsClassLoaderCache override val attachmentsClassLoaderCache: AttachmentsClassLoaderCache get() = this@AbstractNode.attachmentsClassLoaderCache
@Volatile
private lateinit var _networkParameters: NetworkParameters private lateinit var _networkParameters: NetworkParameters
override val networkParameters: NetworkParameters get() = _networkParameters override val networkParameters: NetworkParameters get() = _networkParameters
@ -1272,6 +1359,10 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
val ledgerTransaction = servicesForResolution.specialise(ltx) val ledgerTransaction = servicesForResolution.specialise(ltx)
return verifierFactoryService.apply(ledgerTransaction) return verifierFactoryService.apply(ledgerTransaction)
} }
override fun onNewNetworkParameters(networkParameters: NetworkParameters) {
this._networkParameters = networkParameters
}
} }
} }
@ -1338,13 +1429,15 @@ class FlowStarterImpl(
class ConfigurationException(message: String) : CordaException(message) class ConfigurationException(message: String) : CordaException(message)
@Suppress("LongParameterList")
fun createCordaPersistence(databaseConfig: DatabaseConfig, fun createCordaPersistence(databaseConfig: DatabaseConfig,
wellKnownPartyFromX500Name: (CordaX500Name) -> Party?, wellKnownPartyFromX500Name: (CordaX500Name) -> Party?,
wellKnownPartyFromAnonymous: (AbstractParty) -> Party?, wellKnownPartyFromAnonymous: (AbstractParty) -> Party?,
schemaService: SchemaService, schemaService: SchemaService,
hikariProperties: Properties, hikariProperties: Properties,
cacheFactory: NamedCacheFactory, cacheFactory: NamedCacheFactory,
customClassLoader: ClassLoader?): CordaPersistence { customClassLoader: ClassLoader?,
allowHibernateToManageAppSchema: Boolean = false): CordaPersistence {
// Register the AbstractPartyDescriptor so Hibernate doesn't warn when encountering AbstractParty. Unfortunately // Register the AbstractPartyDescriptor so Hibernate doesn't warn when encountering AbstractParty. Unfortunately
// Hibernate warns about not being able to find a descriptor if we don't provide one, but won't use it by default // Hibernate warns about not being able to find a descriptor if we don't provide one, but won't use it by default
// so we end up providing both descriptor and converter. We should re-examine this in later versions to see if // so we end up providing both descriptor and converter. We should re-examine this in later versions to see if
@ -1355,7 +1448,7 @@ fun createCordaPersistence(databaseConfig: DatabaseConfig,
val jdbcUrl = hikariProperties.getProperty("dataSource.url", "") val jdbcUrl = hikariProperties.getProperty("dataSource.url", "")
return CordaPersistence( return CordaPersistence(
databaseConfig, databaseConfig.exportHibernateJMXStatistics,
schemaService.schemas, schemaService.schemas,
jdbcUrl, jdbcUrl,
cacheFactory, cacheFactory,
@ -1366,14 +1459,20 @@ fun createCordaPersistence(databaseConfig: DatabaseConfig,
// register only the very first exception thrown throughout a chain of logical transactions // register only the very first exception thrown throughout a chain of logical transactions
setException(e) setException(e)
} }
}) },
allowHibernateToManageAppSchema = allowHibernateToManageAppSchema)
} }
fun CordaPersistence.startHikariPool(hikariProperties: Properties, databaseConfig: DatabaseConfig, schemas: Set<MappedSchema>, metricRegistry: MetricRegistry? = null, cordappLoader: CordappLoader? = null, currentDir: Path? = null, ourName: CordaX500Name) { @Suppress("ThrowsCount")
fun CordaPersistence.startHikariPool(
hikariProperties: Properties,
metricRegistry: MetricRegistry? = null,
schemaMigration: (DataSource, Boolean) -> Unit) {
try { try {
val dataSource = DataSourceFactory.createDataSource(hikariProperties, metricRegistry = metricRegistry) val dataSource = DataSourceFactory.createDataSource(hikariProperties, metricRegistry = metricRegistry)
val schemaMigration = SchemaMigration(schemas, dataSource, databaseConfig, cordappLoader, currentDir, ourName) val haveCheckpoints = dataSource.connection.use { DBCheckpointStorage.getCheckpointCount(it) != 0L }
schemaMigration.nodeStartup(dataSource.connection.use { DBCheckpointStorage.getCheckpointCount(it) != 0L })
schemaMigration(dataSource, haveCheckpoints)
start(dataSource) start(dataSource)
} catch (ex: Exception) { } catch (ex: Exception) {
when { when {
@ -1397,6 +1496,14 @@ fun CordaPersistence.startHikariPool(hikariProperties: Properties, databaseConfi
} }
} }
fun SchemaMigration.checkOrUpdate(schemas: Set<MappedSchema>, update: Boolean, haveCheckpoints: Boolean, forceThrowOnMissingMigration: Boolean): SchemaMigration {
if (update)
this.runMigration(haveCheckpoints, schemas, forceThrowOnMissingMigration)
else
this.checkState(schemas, forceThrowOnMissingMigration)
return this
}
fun clientSslOptionsCompatibleWith(nodeRpcOptions: NodeRpcOptions): ClientRpcSslOptions? { fun clientSslOptionsCompatibleWith(nodeRpcOptions: NodeRpcOptions): ClientRpcSslOptions? {
if (!nodeRpcOptions.useSsl || nodeRpcOptions.sslConfig == null) { if (!nodeRpcOptions.useSsl || nodeRpcOptions.sslConfig == null) {

View File

@ -125,7 +125,8 @@ open class Node(configuration: NodeConfiguration,
flowManager: FlowManager = NodeFlowManager(configuration.flowOverrides), flowManager: FlowManager = NodeFlowManager(configuration.flowOverrides),
cacheFactoryPrototype: BindableNamedCacheFactory = DefaultNamedCacheFactory(), cacheFactoryPrototype: BindableNamedCacheFactory = DefaultNamedCacheFactory(),
djvmBootstrapSource: ApiSource = createBootstrapSource(configuration), djvmBootstrapSource: ApiSource = createBootstrapSource(configuration),
djvmCordaSource: UserSource? = createCordaSource(configuration) djvmCordaSource: UserSource? = createCordaSource(configuration),
allowHibernateToManageAppSchema: Boolean = false
) : AbstractNode<NodeInfo>( ) : AbstractNode<NodeInfo>(
configuration, configuration,
createClock(configuration), createClock(configuration),
@ -135,7 +136,8 @@ open class Node(configuration: NodeConfiguration,
// Under normal (non-test execution) it will always be "1" // Under normal (non-test execution) it will always be "1"
AffinityExecutor.ServiceAffinityExecutor("Node thread-${sameVmNodeCounter.incrementAndGet()}", 1), AffinityExecutor.ServiceAffinityExecutor("Node thread-${sameVmNodeCounter.incrementAndGet()}", 1),
djvmBootstrapSource = djvmBootstrapSource, djvmBootstrapSource = djvmBootstrapSource,
djvmCordaSource = djvmCordaSource djvmCordaSource = djvmCordaSource,
allowHibernateToManageAppSchema = allowHibernateToManageAppSchema
) { ) {
override fun createStartedNode(nodeInfo: NodeInfo, rpcOps: CordaRPCOps, notaryService: NotaryService?): NodeInfo = override fun createStartedNode(nodeInfo: NodeInfo, rpcOps: CordaRPCOps, notaryService: NotaryService?): NodeInfo =
@ -559,6 +561,16 @@ open class Node(configuration: NodeConfiguration,
return super.generateAndSaveNodeInfo() return super.generateAndSaveNodeInfo()
} }
override fun runDatabaseMigrationScripts(
updateCoreSchemas: Boolean,
updateAppSchemas: Boolean,
updateAppSchemasWithCheckpoints: Boolean) {
if (allowHibernateToManageAppSchema) {
initialiseSerialization()
}
super.runDatabaseMigrationScripts(updateCoreSchemas, updateAppSchemas, updateAppSchemasWithCheckpoints)
}
override fun start(): NodeInfo { override fun start(): NodeInfo {
registerDefaultExceptionHandler() registerDefaultExceptionHandler()
initialiseSerialization() initialiseSerialization()

View File

@ -76,10 +76,18 @@ open class NodeStartupCli : CordaCliWrapper("corda", "Runs a Corda Node") {
private val justGenerateRpcSslCertsCli by lazy { GenerateRpcSslCertsCli(startup) } private val justGenerateRpcSslCertsCli by lazy { GenerateRpcSslCertsCli(startup) }
private val initialRegistrationCli by lazy { InitialRegistrationCli(startup) } private val initialRegistrationCli by lazy { InitialRegistrationCli(startup) }
private val validateConfigurationCli by lazy { ValidateConfigurationCli() } private val validateConfigurationCli by lazy { ValidateConfigurationCli() }
private val runMigrationScriptsCli by lazy { RunMigrationScriptsCli(startup) }
private val synchroniseAppSchemasCli by lazy { SynchroniseSchemasCli(startup) }
override fun initLogging(): Boolean = this.initLogging(cmdLineOptions.baseDirectory) override fun initLogging(): Boolean = this.initLogging(cmdLineOptions.baseDirectory)
override fun additionalSubCommands() = setOf(networkCacheCli, justGenerateNodeInfoCli, justGenerateRpcSslCertsCli, initialRegistrationCli, validateConfigurationCli) override fun additionalSubCommands() = setOf(networkCacheCli,
justGenerateNodeInfoCli,
justGenerateRpcSslCertsCli,
initialRegistrationCli,
validateConfigurationCli,
runMigrationScriptsCli,
synchroniseAppSchemasCli)
override fun call(): Int { override fun call(): Int {
if (!validateBaseDirectory()) { if (!validateBaseDirectory()) {
@ -201,7 +209,7 @@ open class NodeStartup : NodeStartupLogging {
protected open fun preNetworkRegistration(conf: NodeConfiguration) = Unit protected open fun preNetworkRegistration(conf: NodeConfiguration) = Unit
open fun createNode(conf: NodeConfiguration, versionInfo: VersionInfo): Node = Node(conf, versionInfo) open fun createNode(conf: NodeConfiguration, versionInfo: VersionInfo): Node = Node(conf, versionInfo, allowHibernateToManageAppSchema = cmdLineOptions.allowHibernateToManageAppSchema)
fun startNode(node: Node, startTime: Long) { fun startNode(node: Node, startTime: Long) {
if (node.configuration.devMode) { if (node.configuration.devMode) {

View File

@ -0,0 +1,29 @@
package net.corda.node.internal.subcommands
import net.corda.node.internal.Node
import net.corda.node.internal.NodeCliCommand
import net.corda.node.internal.NodeStartup
import net.corda.node.internal.RunAfterNodeInitialisation
import picocli.CommandLine
class RunMigrationScriptsCli(startup: NodeStartup) : NodeCliCommand("run-migration-scripts", "Run the database migration scripts and create or update schemas", startup) {
@CommandLine.Option(names = ["--core-schemas"], description = ["Manage the core/node schemas"])
var updateCoreSchemas: Boolean = false
@CommandLine.Option(names = ["--app-schemas"], description = ["Manage the CorDapp schemas"])
var updateAppSchemas: Boolean = false
@CommandLine.Option(names = ["--update-app-schema-with-checkpoints"], description = ["Allow updating app schema even if there are suspended flows"])
var updateAppSchemaWithCheckpoints: Boolean = false
override fun runProgram(): Int {
require(updateAppSchemas || updateCoreSchemas) { "Nothing to do: at least one of --core-schemas or --app-schemas must be set" }
return startup.initialiseAndRun(cmdLineOptions, object : RunAfterNodeInitialisation {
override fun run(node: Node) {
node.runDatabaseMigrationScripts(updateCoreSchemas, updateAppSchemas, updateAppSchemaWithCheckpoints)
}
})
}
}

View File

@ -0,0 +1,16 @@
package net.corda.node.internal.subcommands
import net.corda.node.internal.Node
import net.corda.node.internal.NodeCliCommand
import net.corda.node.internal.NodeStartup
import net.corda.node.internal.RunAfterNodeInitialisation
class SynchroniseSchemasCli(startup: NodeStartup) : NodeCliCommand("sync-app-schemas", "Create changelog entries for liquibase files found in CorDapps", startup) {
override fun runProgram(): Int {
return startup.initialiseAndRun(cmdLineOptions, object : RunAfterNodeInitialisation {
override fun run(node: Node) {
node.runSchemaSync()
}
})
}
}

View File

@ -10,9 +10,11 @@ import net.corda.core.identity.CordaX500Name
import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.MappedSchema
import net.corda.node.SimpleClock import net.corda.node.SimpleClock
import net.corda.node.services.identity.PersistentIdentityService import net.corda.node.services.identity.PersistentIdentityService
import net.corda.node.services.persistence.* import net.corda.node.services.persistence.AbstractPartyToX500NameAsStringConverter
import net.corda.node.services.persistence.DBTransactionStorage
import net.corda.node.services.persistence.NodeAttachmentService
import net.corda.node.services.persistence.PublicKeyToTextConverter
import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.nodeapi.internal.persistence.SchemaMigration.Companion.NODE_X500_NAME import net.corda.nodeapi.internal.persistence.SchemaMigration.Companion.NODE_X500_NAME
import java.io.PrintWriter import java.io.PrintWriter
import java.sql.Connection import java.sql.Connection
@ -74,7 +76,6 @@ abstract class CordaMigration : CustomTaskChange {
cacheFactory: MigrationNamedCacheFactory, cacheFactory: MigrationNamedCacheFactory,
identityService: PersistentIdentityService, identityService: PersistentIdentityService,
schema: Set<MappedSchema>): CordaPersistence { schema: Set<MappedSchema>): CordaPersistence {
val configDefaults = DatabaseConfig()
val attributeConverters = listOf( val attributeConverters = listOf(
PublicKeyToTextConverter(), PublicKeyToTextConverter(),
AbstractPartyToX500NameAsStringConverter( AbstractPartyToX500NameAsStringConverter(
@ -83,7 +84,7 @@ abstract class CordaMigration : CustomTaskChange {
) )
// Liquibase handles closing the database connection when migrations are finished. If the connection is closed here, then further // Liquibase handles closing the database connection when migrations are finished. If the connection is closed here, then further
// migrations may fail. // migrations may fail.
return CordaPersistence(configDefaults, schema, jdbcUrl, cacheFactory, attributeConverters, closeConnection = false) return CordaPersistence(false, schema, jdbcUrl, cacheFactory, attributeConverters, closeConnection = false)
} }
override fun validate(database: Database?): ValidationErrors? { override fun validate(database: Database?): ValidationErrors? {

View File

@ -24,6 +24,16 @@ interface CheckpointStorage {
fun updateCheckpoint(id: StateMachineRunId, checkpoint: Checkpoint, serializedFlowState: SerializedBytes<FlowState>?, fun updateCheckpoint(id: StateMachineRunId, checkpoint: Checkpoint, serializedFlowState: SerializedBytes<FlowState>?,
serializedCheckpointState: SerializedBytes<CheckpointState>) serializedCheckpointState: SerializedBytes<CheckpointState>)
/**
* Update an existing checkpoints status ([Checkpoint.status]).
*/
fun updateStatus(runId: StateMachineRunId, flowStatus: Checkpoint.FlowStatus)
/**
* Update an existing checkpoints compatibility flag ([Checkpoint.compatible]).
*/
fun updateCompatible(runId: StateMachineRunId, compatible: Boolean)
/** /**
* Update all persisted checkpoints with status [Checkpoint.FlowStatus.RUNNABLE] or [Checkpoint.FlowStatus.HOSPITALIZED], * Update all persisted checkpoints with status [Checkpoint.FlowStatus.RUNNABLE] or [Checkpoint.FlowStatus.HOSPITALIZED],
* changing the status to [Checkpoint.FlowStatus.PAUSED]. * changing the status to [Checkpoint.FlowStatus.PAUSED].
@ -85,6 +95,4 @@ interface CheckpointStorage {
fun getFlowException(id: StateMachineRunId, throwIfMissing: Boolean = false): Any? fun getFlowException(id: StateMachineRunId, throwIfMissing: Boolean = false): Any?
fun removeFlowException(id: StateMachineRunId): Boolean fun removeFlowException(id: StateMachineRunId): Boolean
fun updateStatus(runId: StateMachineRunId, flowStatus: Checkpoint.FlowStatus)
} }

View File

@ -15,7 +15,6 @@ import net.corda.nodeapi.internal.config.MutualSslConfiguration
import net.corda.nodeapi.internal.config.SslConfiguration import net.corda.nodeapi.internal.config.SslConfiguration
import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.config.User
import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.nodeapi.internal.persistence.SchemaInitializationType
import net.corda.tools.shell.SSHDConfiguration import net.corda.tools.shell.SSHDConfiguration
import java.net.URL import java.net.URL
import java.nio.file.Path import java.nio.file.Path
@ -132,8 +131,6 @@ data class NodeConfigurationImpl(
fun messagingServerExternal(messagingServerAddress: NetworkHostAndPort?) = messagingServerAddress != null fun messagingServerExternal(messagingServerAddress: NetworkHostAndPort?) = messagingServerAddress != null
fun database(devMode: Boolean) = DatabaseConfig( fun database(devMode: Boolean) = DatabaseConfig(
initialiseSchema = devMode,
initialiseAppSchema = if(devMode) SchemaInitializationType.UPDATE else SchemaInitializationType.VALIDATE,
exportHibernateJMXStatistics = devMode exportHibernateJMXStatistics = devMode
) )
} }

View File

@ -14,6 +14,7 @@ import net.corda.common.validation.internal.Validated.Companion.invalid
import net.corda.common.validation.internal.Validated.Companion.valid import net.corda.common.validation.internal.Validated.Companion.valid
import net.corda.core.context.AuthServiceId import net.corda.core.context.AuthServiceId
import net.corda.core.internal.notary.NotaryServiceFlow import net.corda.core.internal.notary.NotaryServiceFlow
import net.corda.node.internal.ConfigurationException
import net.corda.node.services.config.AuthDataSourceType import net.corda.node.services.config.AuthDataSourceType
import net.corda.node.services.config.CertChainPolicyConfig import net.corda.node.services.config.CertChainPolicyConfig
import net.corda.node.services.config.CertChainPolicyType import net.corda.node.services.config.CertChainPolicyType
@ -44,7 +45,6 @@ import net.corda.nodeapi.BrokerRpcSslOptions
import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.config.User
import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.nodeapi.internal.persistence.TransactionIsolationLevel import net.corda.nodeapi.internal.persistence.TransactionIsolationLevel
import net.corda.nodeapi.internal.persistence.SchemaInitializationType
import net.corda.notary.experimental.bftsmart.BFTSmartConfig import net.corda.notary.experimental.bftsmart.BFTSmartConfig
import net.corda.notary.experimental.raft.RaftConfig import net.corda.notary.experimental.raft.RaftConfig
import net.corda.tools.shell.SSHDConfiguration import net.corda.tools.shell.SSHDConfiguration
@ -267,16 +267,32 @@ internal object SSHDConfigurationSpec : Configuration.Specification<SSHDConfigur
override fun parseValid(configuration: Config, options: Configuration.Options): Valid<SSHDConfiguration> = attempt<SSHDConfiguration, IllegalArgumentException> { SSHDConfiguration(configuration.withOptions(options)[port]) } override fun parseValid(configuration: Config, options: Configuration.Options): Valid<SSHDConfiguration> = attempt<SSHDConfiguration, IllegalArgumentException> { SSHDConfiguration(configuration.withOptions(options)[port]) }
} }
enum class SchemaInitializationType{
NONE,
VALIDATE,
UPDATE
}
internal object DatabaseConfigSpec : Configuration.Specification<DatabaseConfig>("DatabaseConfig") { internal object DatabaseConfigSpec : Configuration.Specification<DatabaseConfig>("DatabaseConfig") {
private val initialiseSchema by boolean().optional().withDefaultValue(DatabaseConfig.Defaults.initialiseSchema) private val initialiseSchema by boolean().optional()
private val initialiseAppSchema by enum(SchemaInitializationType::class).optional().withDefaultValue(DatabaseConfig.Defaults.initialiseAppSchema) private val initialiseAppSchema by enum(SchemaInitializationType::class).optional()
private val transactionIsolationLevel by enum(TransactionIsolationLevel::class).optional().withDefaultValue(DatabaseConfig.Defaults.transactionIsolationLevel) private val transactionIsolationLevel by enum(TransactionIsolationLevel::class).optional()
private val exportHibernateJMXStatistics by boolean().optional().withDefaultValue(DatabaseConfig.Defaults.exportHibernateJMXStatistics) private val exportHibernateJMXStatistics by boolean().optional().withDefaultValue(DatabaseConfig.Defaults.exportHibernateJMXStatistics)
private val mappedSchemaCacheSize by long().optional().withDefaultValue(DatabaseConfig.Defaults.mappedSchemaCacheSize) private val mappedSchemaCacheSize by long().optional().withDefaultValue(DatabaseConfig.Defaults.mappedSchemaCacheSize)
override fun parseValid(configuration: Config, options: Configuration.Options): Valid<DatabaseConfig> { override fun parseValid(configuration: Config, options: Configuration.Options): Valid<DatabaseConfig> {
if (initialiseSchema.isSpecifiedBy(configuration)){
throw ConfigurationException("Unsupported configuration database/initialiseSchema - this option has been removed, please use the run-migration-scripts sub-command or the database management tool to modify schemas")
}
if (initialiseAppSchema.isSpecifiedBy(configuration)){
throw ConfigurationException("Unsupported configuration database/initialiseAppSchema - this option has been removed, please use the run-migration-scripts sub-command or the database management tool to modify schemas")
}
if (transactionIsolationLevel.isSpecifiedBy(configuration)){
throw ConfigurationException("Unsupported configuration database/transactionIsolationLevel - this option has been removed and cannot be changed")
}
val config = configuration.withOptions(options) val config = configuration.withOptions(options)
return valid(DatabaseConfig(config[initialiseSchema], config[initialiseAppSchema], config[transactionIsolationLevel], config[exportHibernateJMXStatistics], config[mappedSchemaCacheSize]))
return valid(DatabaseConfig(config[exportHibernateJMXStatistics], config[mappedSchemaCacheSize]))
} }
} }

View File

@ -12,6 +12,7 @@ import net.corda.core.internal.CertRole
import net.corda.core.internal.NamedCacheFactory import net.corda.core.internal.NamedCacheFactory
import net.corda.core.internal.hash import net.corda.core.internal.hash
import net.corda.core.internal.toSet import net.corda.core.internal.toSet
import net.corda.core.node.NotaryInfo
import net.corda.core.node.services.UnknownAnonymousPartyException import net.corda.core.node.services.UnknownAnonymousPartyException
import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.utilities.MAX_HASH_HEX_SIZE import net.corda.core.utilities.MAX_HASH_HEX_SIZE
@ -19,6 +20,7 @@ import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.debug import net.corda.core.utilities.debug
import net.corda.node.services.api.IdentityServiceInternal import net.corda.node.services.api.IdentityServiceInternal
import net.corda.node.services.keys.BasicHSMKeyManagementService import net.corda.node.services.keys.BasicHSMKeyManagementService
import net.corda.node.services.network.NotaryUpdateListener
import net.corda.node.services.persistence.PublicKeyHashToExternalId import net.corda.node.services.persistence.PublicKeyHashToExternalId
import net.corda.node.services.persistence.WritablePublicKeyToOwningIdentityCache import net.corda.node.services.persistence.WritablePublicKeyToOwningIdentityCache
import net.corda.node.utilities.AppendOnlyPersistentMap import net.corda.node.utilities.AppendOnlyPersistentMap
@ -53,7 +55,8 @@ import kotlin.streams.toList
* cached for efficient lookup. * cached for efficient lookup.
*/ */
@ThreadSafe @ThreadSafe
class PersistentIdentityService(cacheFactory: NamedCacheFactory) : SingletonSerializeAsToken(), IdentityServiceInternal { @Suppress("TooManyFunctions")
class PersistentIdentityService(cacheFactory: NamedCacheFactory) : SingletonSerializeAsToken(), IdentityServiceInternal, NotaryUpdateListener {
companion object { companion object {
private val log = contextLogger() private val log = contextLogger()
@ -197,7 +200,8 @@ class PersistentIdentityService(cacheFactory: NamedCacheFactory) : SingletonSeri
override val trustAnchor: TrustAnchor get() = _trustAnchor override val trustAnchor: TrustAnchor get() = _trustAnchor
/** Stores notary identities obtained from the network parameters, for which we don't need to perform a database lookup. */ /** Stores notary identities obtained from the network parameters, for which we don't need to perform a database lookup. */
private val notaryIdentityCache = HashSet<Party>() @Volatile
private var notaryIdentityCache = HashSet<Party>()
// CordaPersistence is not a c'tor parameter to work around the cyclic dependency // CordaPersistence is not a c'tor parameter to work around the cyclic dependency
lateinit var database: CordaPersistence lateinit var database: CordaPersistence
@ -453,4 +457,8 @@ class PersistentIdentityService(cacheFactory: NamedCacheFactory) : SingletonSeri
keys keys
} }
} }
override fun onNewNotaryList(notaries: List<NotaryInfo>) {
notaryIdentityCache = HashSet(notaries.map { it.identity })
}
} }

View File

@ -1,6 +1,7 @@
package net.corda.node.services.network package net.corda.node.services.network
import com.google.common.util.concurrent.MoreExecutors import com.google.common.util.concurrent.MoreExecutors
import net.corda.cliutils.ExitCodes
import net.corda.core.CordaRuntimeException import net.corda.core.CordaRuntimeException
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.SignedData import net.corda.core.crypto.SignedData
@ -62,7 +63,7 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal,
private val baseDirectory: Path, private val baseDirectory: Path,
private val extraNetworkMapKeys: List<UUID>, private val extraNetworkMapKeys: List<UUID>,
private val networkParametersStorage: NetworkParametersStorage private val networkParametersStorage: NetworkParametersStorage
) : AutoCloseable { ) : AutoCloseable, NetworkParameterUpdateListener {
companion object { companion object {
private val logger = contextLogger() private val logger = contextLogger()
private val defaultRetryInterval = 1.minutes private val defaultRetryInterval = 1.minutes
@ -77,12 +78,15 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal,
private val fileWatcherSubscription = AtomicReference<Subscription?>() private val fileWatcherSubscription = AtomicReference<Subscription?>()
private var autoAcceptNetworkParameters: Boolean = true private var autoAcceptNetworkParameters: Boolean = true
private lateinit var trustRoot: X509Certificate private lateinit var trustRoot: X509Certificate
@Volatile
private lateinit var currentParametersHash: SecureHash private lateinit var currentParametersHash: SecureHash
private lateinit var ourNodeInfo: SignedNodeInfo private lateinit var ourNodeInfo: SignedNodeInfo
private lateinit var ourNodeInfoHash: SecureHash private lateinit var ourNodeInfoHash: SecureHash
private lateinit var networkParameters: NetworkParameters private lateinit var networkParameters: NetworkParameters
private lateinit var keyManagementService: KeyManagementService private lateinit var keyManagementService: KeyManagementService
private lateinit var excludedAutoAcceptNetworkParameters: Set<String> private lateinit var excludedAutoAcceptNetworkParameters: Set<String>
private var networkParametersHotloader: NetworkParametersHotloader? = null
override fun close() { override fun close() {
fileWatcherSubscription.updateAndGet { subscription -> fileWatcherSubscription.updateAndGet { subscription ->
@ -95,13 +99,15 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal,
} }
MoreExecutors.shutdownAndAwaitTermination(networkMapPoller, 50, TimeUnit.SECONDS) MoreExecutors.shutdownAndAwaitTermination(networkMapPoller, 50, TimeUnit.SECONDS)
} }
@Suppress("LongParameterList")
fun start(trustRoot: X509Certificate, fun start(trustRoot: X509Certificate,
currentParametersHash: SecureHash, currentParametersHash: SecureHash,
ourNodeInfo: SignedNodeInfo, ourNodeInfo: SignedNodeInfo,
networkParameters: NetworkParameters, networkParameters: NetworkParameters,
keyManagementService: KeyManagementService, keyManagementService: KeyManagementService,
networkParameterAcceptanceSettings: NetworkParameterAcceptanceSettings) { networkParameterAcceptanceSettings: NetworkParameterAcceptanceSettings,
networkParametersHotloader: NetworkParametersHotloader?
) {
fileWatcherSubscription.updateAndGet { subscription -> fileWatcherSubscription.updateAndGet { subscription ->
require(subscription == null) { "Should not call this method twice" } require(subscription == null) { "Should not call this method twice" }
this.trustRoot = trustRoot this.trustRoot = trustRoot
@ -112,6 +118,8 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal,
this.keyManagementService = keyManagementService this.keyManagementService = keyManagementService
this.autoAcceptNetworkParameters = networkParameterAcceptanceSettings.autoAcceptEnabled this.autoAcceptNetworkParameters = networkParameterAcceptanceSettings.autoAcceptEnabled
this.excludedAutoAcceptNetworkParameters = networkParameterAcceptanceSettings.excludedAutoAcceptableParameters this.excludedAutoAcceptNetworkParameters = networkParameterAcceptanceSettings.excludedAutoAcceptableParameters
this.networkParametersHotloader = networkParametersHotloader
val autoAcceptNetworkParametersNames = autoAcceptablePropertyNames - excludedAutoAcceptNetworkParameters val autoAcceptNetworkParametersNames = autoAcceptablePropertyNames - excludedAutoAcceptNetworkParameters
if (autoAcceptNetworkParameters && autoAcceptNetworkParametersNames.isNotEmpty()) { if (autoAcceptNetworkParameters && autoAcceptNetworkParametersNames.isNotEmpty()) {
@ -180,7 +188,7 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal,
val additionalHashes = getPrivateNetworkNodeHashes(version) val additionalHashes = getPrivateNetworkNodeHashes(version)
val allHashesFromNetworkMap = (globalNetworkMap.nodeInfoHashes + additionalHashes).toSet() val allHashesFromNetworkMap = (globalNetworkMap.nodeInfoHashes + additionalHashes).toSet()
if (currentParametersHash != globalNetworkMap.networkParameterHash) { if (currentParametersHash != globalNetworkMap.networkParameterHash) {
exitOnParametersMismatch(globalNetworkMap) hotloadOrExitOnParametersMismatch(globalNetworkMap)
} }
// Calculate any nodes that are now gone and remove _only_ them from the cache // Calculate any nodes that are now gone and remove _only_ them from the cache
// NOTE: We won't remove them until after the add/update cycle as only then will we definitely know which nodes are no longer // NOTE: We won't remove them until after the add/update cycle as only then will we definitely know which nodes are no longer
@ -276,22 +284,26 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal,
} }
} }
private fun exitOnParametersMismatch(networkMap: NetworkMap) { private fun hotloadOrExitOnParametersMismatch(networkMap: NetworkMap) {
val updatesFile = baseDirectory / NETWORK_PARAMS_UPDATE_FILE_NAME val updatesFile = baseDirectory / NETWORK_PARAMS_UPDATE_FILE_NAME
val acceptedHash = if (updatesFile.exists()) updatesFile.readObject<SignedNetworkParameters>().raw.hash else null val newParameterHash = networkMap.networkParameterHash
val exitCode = if (acceptedHash == networkMap.networkParameterHash) { val nodeAcceptedNewParameters = updatesFile.exists() && newParameterHash == updatesFile.readObject<SignedNetworkParameters>().raw.hash
logger.info("Flag day occurred. Network map switched to the new network parameters: " +
"${networkMap.networkParameterHash}. Node will shutdown now and needs to be started again.") if (!nodeAcceptedNewParameters) {
0
} else {
// TODO This needs special handling (node omitted update process or didn't accept new parameters)
logger.error( logger.error(
"""Node is using network parameters with hash $currentParametersHash but the network map is advertising ${networkMap.networkParameterHash}. """Node is using network parameters with hash $currentParametersHash but the network map is advertising ${networkMap.networkParameterHash}.
To resolve this mismatch, and move to the current parameters, delete the $NETWORK_PARAMS_FILE_NAME file from the node's directory and restart. To resolve this mismatch, and move to the current parameters, delete the $NETWORK_PARAMS_FILE_NAME file from the node's directory and restart.
The node will shutdown now.""") The node will shutdown now.""")
1 exitProcess(ExitCodes.FAILURE)
} }
exitProcess(exitCode)
val hotloadSucceeded = networkParametersHotloader!!.attemptHotload(newParameterHash)
if (!hotloadSucceeded) {
logger.info("Flag day occurred. Network map switched to the new network parameters: " +
"${networkMap.networkParameterHash}. Node will shutdown now and needs to be started again.")
exitProcess(ExitCodes.SUCCESS)
}
currentParametersHash = newParameterHash
} }
private fun handleUpdateNetworkParameters(networkMapClient: NetworkMapClient, update: ParametersUpdate) { private fun handleUpdateNetworkParameters(networkMapClient: NetworkMapClient, update: ParametersUpdate) {
@ -340,6 +352,10 @@ The node will shutdown now.""")
throw OutdatedNetworkParameterHashException(parametersHash, newParametersHash) throw OutdatedNetworkParameterHashException(parametersHash, newParametersHash)
} }
} }
override fun onNewNetworkParameters(networkParameters: NetworkParameters) {
this.networkParameters = networkParameters
}
} }
private val memberPropertyPartition = NetworkParameters::class.declaredMemberProperties.partition { it.isAutoAcceptable() } private val memberPropertyPartition = NetworkParameters::class.declaredMemberProperties.partition { it.isAutoAcceptable() }
@ -360,7 +376,7 @@ internal fun NetworkParameters.canAutoAccept(newNetworkParameters: NetworkParame
private fun KProperty1<out NetworkParameters, Any?>.isAutoAcceptable(): Boolean = findAnnotation<AutoAcceptable>() != null private fun KProperty1<out NetworkParameters, Any?>.isAutoAcceptable(): Boolean = findAnnotation<AutoAcceptable>() != null
private fun NetworkParameters.valueChanged(newNetworkParameters: NetworkParameters, getter: Method?): Boolean { internal fun NetworkParameters.valueChanged(newNetworkParameters: NetworkParameters, getter: Method?): Boolean {
val propertyValue = getter?.invoke(this) val propertyValue = getter?.invoke(this)
val newPropertyValue = getter?.invoke(newNetworkParameters) val newPropertyValue = getter?.invoke(newNetworkParameters)
return propertyValue != newPropertyValue return propertyValue != newPropertyValue

View File

@ -0,0 +1,11 @@
package net.corda.node.services.network
import net.corda.core.node.NetworkParameters
/**
* When network parameters change on a flag day, onNewNetworkParameters will be invoked with the new parameters.
* Used inside {@link net.corda.node.services.network.NetworkParametersUpdater}
*/
interface NetworkParameterUpdateListener {
fun onNewNetworkParameters(networkParameters: NetworkParameters)
}

View File

@ -0,0 +1,88 @@
package net.corda.node.services.network
import net.corda.core.crypto.SecureHash
import net.corda.core.internal.NetworkParametersStorage
import net.corda.core.node.NetworkParameters
import net.corda.core.node.NotaryInfo
import net.corda.core.utilities.contextLogger
import net.corda.node.internal.NetworkParametersReader
import net.corda.nodeapi.internal.network.verifiedNetworkParametersCert
import java.security.cert.X509Certificate
import kotlin.reflect.full.declaredMemberProperties
import kotlin.reflect.jvm.javaGetter
/**
* This class is responsible for hotloading new network parameters or shut down the node if it's not possible.
* Currently only hotloading notary changes are supported.
*/
class NetworkParametersHotloader(private val networkMapClient: NetworkMapClient,
private val trustRoot: X509Certificate,
@Volatile private var networkParameters: NetworkParameters,
private val networkParametersReader: NetworkParametersReader,
private val networkParametersStorage: NetworkParametersStorage) {
companion object {
private val logger = contextLogger()
private val alwaysHotloadable = listOf(NetworkParameters::epoch, NetworkParameters::modifiedTime)
}
private val networkParameterUpdateListeners = mutableListOf<NetworkParameterUpdateListener>()
private val notaryUpdateListeners = mutableListOf<NotaryUpdateListener>()
fun addNetworkParametersChangedListeners(listener: NetworkParameterUpdateListener) {
networkParameterUpdateListeners.add(listener)
}
fun addNotaryUpdateListener(listener: NotaryUpdateListener) {
notaryUpdateListeners.add(listener)
}
private fun notifyListenersFor(notaries: List<NotaryInfo>) = notaryUpdateListeners.forEach { it.onNewNotaryList(notaries) }
private fun notifyListenersFor(networkParameters: NetworkParameters) = networkParameterUpdateListeners.forEach { it.onNewNetworkParameters(networkParameters) }
fun attemptHotload(newNetworkParameterHash: SecureHash): Boolean {
val newSignedNetParams = networkMapClient.getNetworkParameters(newNetworkParameterHash)
val newNetParams = newSignedNetParams.verifiedNetworkParametersCert(trustRoot)
if (canHotload(newNetParams)) {
logger.info("All changed parameters are hotloadable")
hotloadParameters(newNetParams)
return true
} else {
return false
}
}
/**
* Ignoring always hotloadable properties (epoch, modifiedTime) return true if the notary is the only property that is different in the new network parameters
*/
private fun canHotload(newNetworkParameters: NetworkParameters): Boolean {
val propertiesChanged = NetworkParameters::class.declaredMemberProperties
.minus(alwaysHotloadable)
.filter { networkParameters.valueChanged(newNetworkParameters, it.javaGetter) }
logger.info("Updated NetworkParameters properties: $propertiesChanged")
val noPropertiesChanged = propertiesChanged.isEmpty()
val onlyNotariesChanged = propertiesChanged == listOf(NetworkParameters::notaries)
return when {
noPropertiesChanged -> true
onlyNotariesChanged -> true
else -> false
}
}
/**
* Update local networkParameters and currentParametersHash with new values.
* Notify all listeners for network parameter changes
*/
private fun hotloadParameters(newNetworkParameters: NetworkParameters) {
networkParameters = newNetworkParameters
val networkParametersAndSigned = networkParametersReader.read()
networkParametersStorage.setCurrentParameters(networkParametersAndSigned.signed, trustRoot)
notifyListenersFor(newNetworkParameters)
notifyListenersFor(newNetworkParameters.notaries)
}
}

View File

@ -0,0 +1,11 @@
package net.corda.node.services.network
import net.corda.core.node.NotaryInfo
/**
* When notaries inside network parameters change on a flag day, onNewNotaryList will be invoked with the new notary list.
* Used inside {@link net.corda.node.services.network.NetworkParametersUpdater}
*/
interface NotaryUpdateListener {
fun onNewNotaryList(notaries: List<NotaryInfo>)
}

View File

@ -38,9 +38,10 @@ import javax.persistence.PersistenceException
/** Database-based network map cache. */ /** Database-based network map cache. */
@ThreadSafe @ThreadSafe
@Suppress("TooManyFunctions")
open class PersistentNetworkMapCache(cacheFactory: NamedCacheFactory, open class PersistentNetworkMapCache(cacheFactory: NamedCacheFactory,
private val database: CordaPersistence, private val database: CordaPersistence,
private val identityService: IdentityService) : NetworkMapCacheInternal, SingletonSerializeAsToken() { private val identityService: IdentityService) : NetworkMapCacheInternal, SingletonSerializeAsToken(), NotaryUpdateListener {
companion object { companion object {
private val logger = contextLogger() private val logger = contextLogger()
@ -53,6 +54,7 @@ open class PersistentNetworkMapCache(cacheFactory: NamedCacheFactory,
override val nodeReady: OpenFuture<Void?> = openFuture() override val nodeReady: OpenFuture<Void?> = openFuture()
@Volatile
private lateinit var notaries: List<NotaryInfo> private lateinit var notaries: List<NotaryInfo>
override val notaryIdentities: List<Party> get() = notaries.map { it.identity } override val notaryIdentities: List<Party> get() = notaries.map { it.identity }
@ -386,4 +388,8 @@ open class PersistentNetworkMapCache(cacheFactory: NamedCacheFactory,
for (nodeInfo in result) session.remove(nodeInfo) for (nodeInfo in result) session.remove(nodeInfo)
} }
} }
override fun onNewNotaryList(notaries: List<NotaryInfo>) {
this.notaries = notaries
}
} }

View File

@ -599,6 +599,11 @@ class DBCheckpointStorage(
currentDBSession().createNativeQuery(update).executeUpdate() currentDBSession().createNativeQuery(update).executeUpdate()
} }
override fun updateCompatible(runId: StateMachineRunId, compatible: Boolean) {
val update = "Update ${NODE_DATABASE_PREFIX}checkpoints set compatible = $compatible where flow_id = '${runId.uuid}'"
currentDBSession().createNativeQuery(update).executeUpdate()
}
private fun createDBFlowMetadata(flowId: String, checkpoint: Checkpoint, now: Instant): DBFlowMetadata { private fun createDBFlowMetadata(flowId: String, checkpoint: Checkpoint, now: Instant): DBFlowMetadata {
val context = checkpoint.checkpointState.invocationContext val context = checkpoint.checkpointState.invocationContext
val flowInfo = checkpoint.checkpointState.subFlowStack.first() val flowInfo = checkpoint.checkpointState.subFlowStack.first()

View File

@ -95,7 +95,9 @@ class DBTransactionStorage(private val database: CordaPersistence, cacheFactory:
} }
} }
private companion object { internal companion object {
const val TRANSACTION_ALREADY_IN_PROGRESS_WARNING = "trackTransaction is called with an already existing, open DB transaction. As a result, there might be transactions missing from the returned data feed, because of race conditions."
// Rough estimate for the average of a public key and the transaction metadata - hard to get exact figures here, // Rough estimate for the average of a public key and the transaction metadata - hard to get exact figures here,
// as public keys can vary in size a lot, and if someone else is holding a reference to the key, it won't add // as public keys can vary in size a lot, and if someone else is holding a reference to the key, it won't add
// to the memory pressure at all here. // to the memory pressure at all here.
@ -111,7 +113,7 @@ class DBTransactionStorage(private val database: CordaPersistence, cacheFactory:
} }
} }
fun createTransactionsMap(cacheFactory: NamedCacheFactory, clock: CordaClock) private fun createTransactionsMap(cacheFactory: NamedCacheFactory, clock: CordaClock)
: AppendOnlyPersistentMapBase<SecureHash, TxCacheValue, DBTransaction, String> { : AppendOnlyPersistentMapBase<SecureHash, TxCacheValue, DBTransaction, String> {
return WeightBasedAppendOnlyPersistentMap<SecureHash, TxCacheValue, DBTransaction, String>( return WeightBasedAppendOnlyPersistentMap<SecureHash, TxCacheValue, DBTransaction, String>(
cacheFactory = cacheFactory, cacheFactory = cacheFactory,
@ -221,12 +223,22 @@ class DBTransactionStorage(private val database: CordaPersistence, cacheFactory:
} }
override fun trackTransaction(id: SecureHash): CordaFuture<SignedTransaction> { override fun trackTransaction(id: SecureHash): CordaFuture<SignedTransaction> {
val (transaction, warning) = trackTransactionInternal(id)
if (contextTransactionOrNull != null) { warning?.also { log.warn(it) }
log.warn("trackTransaction is called with an already existing, open DB transaction. As a result, there might be transactions missing from the returned data feed, because of race conditions.") return transaction
} }
return trackTransactionWithNoWarning(id) /**
* @return a pair of the signed transaction, and a string containing any warning.
*/
internal fun trackTransactionInternal(id: SecureHash): Pair<CordaFuture<SignedTransaction>, String?> {
val warning: String? = if (contextTransactionOrNull != null) {
TRANSACTION_ALREADY_IN_PROGRESS_WARNING
} else {
null
}
return Pair(trackTransactionWithNoWarning(id), warning)
} }
override fun trackTransactionWithNoWarning(id: SecureHash): CordaFuture<SignedTransaction> { override fun trackTransactionWithNoWarning(id: SecureHash): CordaFuture<SignedTransaction> {

View File

@ -62,13 +62,12 @@ class NodeSchemaService(private val extraSchemas: Set<MappedSchema> = emptySet()
NodeInfoSchemaV1, NodeInfoSchemaV1,
NodeCoreV1) NodeCoreV1)
fun internalSchemas() = requiredSchemas + extraSchemas.filter { schema -> val internalSchemas = requiredSchemas + extraSchemas.filter { schema ->
// when mapped schemas from the finance module are present, they are considered as internal ones
schema::class.qualifiedName == "net.corda.finance.schemas.CashSchemaV1" ||
schema::class.qualifiedName == "net.corda.finance.schemas.CommercialPaperSchemaV1" ||
schema::class.qualifiedName?.startsWith("net.corda.notary.") ?: false schema::class.qualifiedName?.startsWith("net.corda.notary.") ?: false
} }
val appSchemas = extraSchemas - internalSchemas
override val schemas: Set<MappedSchema> = requiredSchemas + extraSchemas override val schemas: Set<MappedSchema> = requiredSchemas + extraSchemas
// Currently returns all schemas supported by the state, with no filtering or enrichment. // Currently returns all schemas supported by the state, with no filtering or enrichment.

View File

@ -57,6 +57,11 @@ sealed class Action {
*/ */
data class PersistCheckpoint(val id: StateMachineRunId, val checkpoint: Checkpoint, val isCheckpointUpdate: Boolean) : Action() data class PersistCheckpoint(val id: StateMachineRunId, val checkpoint: Checkpoint, val isCheckpointUpdate: Boolean) : Action()
/**
* Update only the [status] of the checkpoint with [id].
*/
data class UpdateFlowStatus(val id: StateMachineRunId, val status: Checkpoint.FlowStatus): Action()
/** /**
* Remove the checkpoint corresponding to [id]. [mayHavePersistentResults] denotes that at the time of injecting a [RemoveCheckpoint] * Remove the checkpoint corresponding to [id]. [mayHavePersistentResults] denotes that at the time of injecting a [RemoveCheckpoint]
* the flow could have persisted its database result or exception. * the flow could have persisted its database result or exception.
@ -108,6 +113,11 @@ sealed class Action {
val lastState: StateMachineState val lastState: StateMachineState
) : Action() ) : Action()
/**
* Move the flow corresponding to [flowId] to paused.
*/
data class MoveFlowToPaused(val currentState: StateMachineState) : Action()
/** /**
* Schedule [event] to self. * Schedule [event] to self.
*/ */

View File

@ -67,6 +67,8 @@ internal class ActionExecutorImpl(
is Action.RetryFlowFromSafePoint -> executeRetryFlowFromSafePoint(action) is Action.RetryFlowFromSafePoint -> executeRetryFlowFromSafePoint(action)
is Action.ScheduleFlowTimeout -> scheduleFlowTimeout(action) is Action.ScheduleFlowTimeout -> scheduleFlowTimeout(action)
is Action.CancelFlowTimeout -> cancelFlowTimeout(action) is Action.CancelFlowTimeout -> cancelFlowTimeout(action)
is Action.MoveFlowToPaused -> executeMoveFlowToPaused(action)
is Action.UpdateFlowStatus -> executeUpdateFlowStatus(action)
} }
} }
private fun executeReleaseSoftLocks(action: Action.ReleaseSoftLocks) { private fun executeReleaseSoftLocks(action: Action.ReleaseSoftLocks) {
@ -99,6 +101,11 @@ internal class ActionExecutorImpl(
} }
} }
@Suspendable
private fun executeUpdateFlowStatus(action: Action.UpdateFlowStatus) {
checkpointStorage.updateStatus(action.id, action.status)
}
@Suspendable @Suspendable
private fun executePersistDeduplicationIds(action: Action.PersistDeduplicationFacts) { private fun executePersistDeduplicationIds(action: Action.PersistDeduplicationFacts) {
for (handle in action.deduplicationHandlers) { for (handle in action.deduplicationHandlers) {
@ -191,6 +198,11 @@ internal class ActionExecutorImpl(
stateMachineManager.removeFlow(action.flowId, action.removalReason, action.lastState) stateMachineManager.removeFlow(action.flowId, action.removalReason, action.lastState)
} }
@Suspendable
private fun executeMoveFlowToPaused(action: Action.MoveFlowToPaused) {
stateMachineManager.moveFlowToPaused(action.currentState)
}
@Suspendable @Suspendable
@Throws(SQLException::class) @Throws(SQLException::class)
private fun executeCreateTransaction() { private fun executeCreateTransaction() {

View File

@ -139,7 +139,7 @@ sealed class Event {
data class AsyncOperationCompletion(val returnValue: Any?) : Event() data class AsyncOperationCompletion(val returnValue: Any?) : Event()
/** /**
* Signals the faiure of a [FlowAsyncOperation]. * Signals the failure of a [FlowAsyncOperation].
* *
* Scheduling is triggered by the service that completes the future returned by the async operation. * Scheduling is triggered by the service that completes the future returned by the async operation.
* *
@ -179,6 +179,20 @@ sealed class Event {
override fun toString() = "WakeUpSleepyFlow" override fun toString() = "WakeUpSleepyFlow"
} }
/**
* Pause the flow.
*/
object Pause: Event() {
override fun toString() = "Pause"
}
/**
* Terminate the specified [sessions], removing them from in-memory datastructures.
*
* @param sessions The sessions to terminate
*/
data class TerminateSessions(val sessions: Set<SessionId>) : Event()
/** /**
* Indicates that an event was generated by an external event and that external event needs to be replayed if we retry the flow, * Indicates that an event was generated by an external event and that external event needs to be replayed if we retry the flow,
* even if it has not yet been processed and placed on the pending de-duplication handlers list. * even if it has not yet been processed and placed on the pending de-duplication handlers list.

View File

@ -19,22 +19,25 @@ import net.corda.core.utilities.contextLogger
import net.corda.node.services.api.CheckpointStorage import net.corda.node.services.api.CheckpointStorage
import net.corda.node.services.api.ServiceHubInternal import net.corda.node.services.api.ServiceHubInternal
import net.corda.node.services.messaging.DeduplicationHandler import net.corda.node.services.messaging.DeduplicationHandler
import net.corda.node.services.statemachine.FlowStateMachineImpl.Companion.currentStateMachine
import net.corda.node.services.statemachine.transitions.StateMachine import net.corda.node.services.statemachine.transitions.StateMachine
import net.corda.node.utilities.isEnabledTimedFlow import net.corda.node.utilities.isEnabledTimedFlow
import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.CordaPersistence
import org.apache.activemq.artemis.utils.ReusableLatch import org.apache.activemq.artemis.utils.ReusableLatch
import java.security.SecureRandom import java.security.SecureRandom
import java.util.concurrent.Semaphore
class Flow<A>(val fiber: FlowStateMachineImpl<A>, val resultFuture: OpenFuture<Any?>) class Flow<A>(val fiber: FlowStateMachineImpl<A>, val resultFuture: OpenFuture<Any?>)
class NonResidentFlow(val runId: StateMachineRunId, val checkpoint: Checkpoint) { data class NonResidentFlow(
val resultFuture: OpenFuture<Any?> = openFuture() val runId: StateMachineRunId,
var checkpoint: Checkpoint,
val resultFuture: OpenFuture<Any?> = openFuture(),
val resumable: Boolean = true
) {
val events = mutableListOf<ExternalEvent>()
val externalEvents = mutableListOf<Event.DeliverSessionMessage>() fun addExternalEvent(message: ExternalEvent) {
events.add(message)
fun addExternalEvent(message: Event.DeliverSessionMessage) {
externalEvents.add(message)
} }
} }
@ -67,31 +70,49 @@ class FlowCreator(
} }
else -> nonResidentFlow.checkpoint else -> nonResidentFlow.checkpoint
} }
return createFlowFromCheckpoint(nonResidentFlow.runId, checkpoint, nonResidentFlow.resultFuture) return createFlowFromCheckpoint(nonResidentFlow.runId, checkpoint, resultFuture = nonResidentFlow.resultFuture)
} }
@Suppress("LongParameterList")
fun createFlowFromCheckpoint( fun createFlowFromCheckpoint(
runId: StateMachineRunId, runId: StateMachineRunId,
oldCheckpoint: Checkpoint, oldCheckpoint: Checkpoint,
reloadCheckpointAfterSuspendCount: Int? = null,
lock: Semaphore = Semaphore(1),
resultFuture: OpenFuture<Any?> = openFuture(), resultFuture: OpenFuture<Any?> = openFuture(),
reloadCheckpointAfterSuspendCount: Int? = null firstRestore: Boolean = true
): Flow<*>? { ): Flow<*>? {
val checkpoint = oldCheckpoint.copy(status = Checkpoint.FlowStatus.RUNNABLE) val fiber = oldCheckpoint.getFiberFromCheckpoint(runId, firstRestore)
val fiber = checkpoint.getFiberFromCheckpoint(runId) ?: return null var checkpoint = oldCheckpoint
if (fiber == null) {
updateCompatibleInDb(runId, false)
return null
} else if (!oldCheckpoint.compatible) {
updateCompatibleInDb(runId, true)
checkpoint = checkpoint.copy(compatible = true)
}
checkpoint = checkpoint.copy(status = Checkpoint.FlowStatus.RUNNABLE)
fiber.logic.stateMachine = fiber fiber.logic.stateMachine = fiber
verifyFlowLogicIsSuspendable(fiber.logic) verifyFlowLogicIsSuspendable(fiber.logic)
val state = createStateMachineState( fiber.transientValues = createTransientValues(runId, resultFuture)
fiber.transientState = createStateMachineState(
checkpoint = checkpoint, checkpoint = checkpoint,
fiber = fiber, fiber = fiber,
anyCheckpointPersisted = true, anyCheckpointPersisted = true,
reloadCheckpointAfterSuspendCount = reloadCheckpointAfterSuspendCount reloadCheckpointAfterSuspendCount = reloadCheckpointAfterSuspendCount
?: if (reloadCheckpointAfterSuspend) checkpoint.checkpointState.numberOfSuspends else null ?: if (reloadCheckpointAfterSuspend) checkpoint.checkpointState.numberOfSuspends else null,
lock = lock
) )
fiber.transientValues = createTransientValues(runId, resultFuture)
fiber.transientState = state
return Flow(fiber, resultFuture) return Flow(fiber, resultFuture)
} }
private fun updateCompatibleInDb(runId: StateMachineRunId, compatible: Boolean) {
database.transaction {
checkpointStorage.updateCompatible(runId, compatible)
}
}
@Suppress("LongParameterList") @Suppress("LongParameterList")
fun <A> createFlowFromLogic( fun <A> createFlowFromLogic(
flowId: StateMachineRunId, flowId: StateMachineRunId,
@ -127,6 +148,7 @@ class FlowCreator(
fiber = flowStateMachineImpl, fiber = flowStateMachineImpl,
anyCheckpointPersisted = existingCheckpoint != null, anyCheckpointPersisted = existingCheckpoint != null,
reloadCheckpointAfterSuspendCount = if (reloadCheckpointAfterSuspend) 0 else null, reloadCheckpointAfterSuspendCount = if (reloadCheckpointAfterSuspend) 0 else null,
lock = Semaphore(1),
deduplicationHandler = deduplicationHandler, deduplicationHandler = deduplicationHandler,
senderUUID = senderUUID senderUUID = senderUUID
) )
@ -134,36 +156,45 @@ class FlowCreator(
return Flow(flowStateMachineImpl, resultFuture) return Flow(flowStateMachineImpl, resultFuture)
} }
private fun Checkpoint.getFiberFromCheckpoint(runId: StateMachineRunId): FlowStateMachineImpl<*>? { @Suppress("TooGenericExceptionCaught")
return when (this.flowState) { private fun Checkpoint.getFiberFromCheckpoint(runId: StateMachineRunId, firstRestore: Boolean): FlowStateMachineImpl<*>? {
try {
return when(flowState) {
is FlowState.Unstarted -> { is FlowState.Unstarted -> {
val logic = tryCheckpointDeserialize(this.flowState.frozenFlowLogic, runId) ?: return null val logic = deserializeFlowState(flowState.frozenFlowLogic)
FlowStateMachineImpl(runId, logic, scheduler) FlowStateMachineImpl(runId, logic, scheduler)
} }
is FlowState.Started -> tryCheckpointDeserialize(this.flowState.frozenFiber, runId) ?: return null is FlowState.Started -> deserializeFlowState(flowState.frozenFiber)
// Places calling this function is rely on it to return null if the flow cannot be created from the checkpoint. // Places calling this function is rely on it to return null if the flow cannot be created from the checkpoint.
else -> null else -> return null
} }
}
@Suppress("TooGenericExceptionCaught")
private inline fun <reified T : Any> tryCheckpointDeserialize(bytes: SerializedBytes<T>, flowId: StateMachineRunId): T? {
return try {
bytes.checkpointDeserialize(context = checkpointSerializationContext)
} catch (e: Exception) { } catch (e: Exception) {
if (reloadCheckpointAfterSuspend && currentStateMachine() != null) { if (reloadCheckpointAfterSuspend && FlowStateMachineImpl.currentStateMachine() != null) {
logger.error( logger.error(
"Unable to deserialize checkpoint for flow $flowId. [reloadCheckpointAfterSuspend] is turned on, throwing exception", "Unable to deserialize checkpoint for flow $runId. [reloadCheckpointAfterSuspend] is turned on, throwing exception",
e e
) )
throw ReloadFlowFromCheckpointException(e) throw ReloadFlowFromCheckpointException(e)
} else { } else {
logger.error("Unable to deserialize checkpoint for flow $flowId. Something is very wrong and this flow will be ignored.", e) logSerializationError(firstRestore, runId, e)
null return null
} }
} }
} }
private inline fun <reified T : Any> deserializeFlowState(bytes: SerializedBytes<T>): T {
return bytes.checkpointDeserialize(context = checkpointSerializationContext)
}
private fun logSerializationError(firstRestore: Boolean, flowId: StateMachineRunId, exception: Exception) {
if (firstRestore) {
logger.warn("Flow with id $flowId could not be restored from its checkpoint. Normally this means that a CorDapp has been" +
" upgraded without draining the node. To run this flow restart the node after downgrading the CorDapp.", exception)
} else {
logger.error("Unable to deserialize fiber for flow $flowId. Something is very wrong and this flow will be ignored.", exception)
}
}
private fun verifyFlowLogicIsSuspendable(logic: FlowLogic<Any?>) { private fun verifyFlowLogicIsSuspendable(logic: FlowLogic<Any?>) {
// Quasar requires (in Java 8) that at least the call method be annotated suspendable. Unfortunately, it's // Quasar requires (in Java 8) that at least the call method be annotated suspendable. Unfortunately, it's
// easy to forget to add this when creating a new flow, so we check here to give the user a better error. // easy to forget to add this when creating a new flow, so we check here to give the user a better error.
@ -198,6 +229,7 @@ class FlowCreator(
fiber: FlowStateMachineImpl<*>, fiber: FlowStateMachineImpl<*>,
anyCheckpointPersisted: Boolean, anyCheckpointPersisted: Boolean,
reloadCheckpointAfterSuspendCount: Int?, reloadCheckpointAfterSuspendCount: Int?,
lock: Semaphore,
deduplicationHandler: DeduplicationHandler? = null, deduplicationHandler: DeduplicationHandler? = null,
senderUUID: String? = null senderUUID: String? = null
): StateMachineState { ): StateMachineState {
@ -213,7 +245,8 @@ class FlowCreator(
isKilled = false, isKilled = false,
flowLogic = fiber.logic, flowLogic = fiber.logic,
senderUUID = senderUUID, senderUUID = senderUUID,
reloadCheckpointAfterSuspendCount = reloadCheckpointAfterSuspendCount reloadCheckpointAfterSuspendCount = reloadCheckpointAfterSuspendCount,
lock = lock
) )
} }
} }

View File

@ -157,6 +157,16 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
internal val softLockedStates = mutableSetOf<StateRef>() internal val softLockedStates = mutableSetOf<StateRef>()
internal inline fun <RESULT> withFlowLock(block: FlowStateMachineImpl<R>.() -> RESULT): RESULT {
transientState.lock.acquire()
return try {
block(this)
} finally {
transientState.lock.release()
}
}
/** /**
* Processes an event by creating the associated transition and executing it using the given executor. * Processes an event by creating the associated transition and executing it using the given executor.
* Try to avoid using this directly, instead use [processEventsUntilFlowIsResumed] or [processEventImmediately] * Try to avoid using this directly, instead use [processEventsUntilFlowIsResumed] or [processEventImmediately]
@ -164,20 +174,23 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
*/ */
@Suspendable @Suspendable
private fun processEvent(transitionExecutor: TransitionExecutor, event: Event): FlowContinuation { private fun processEvent(transitionExecutor: TransitionExecutor, event: Event): FlowContinuation {
return withFlowLock {
setLoggingContext() setLoggingContext()
val stateMachine = transientValues.stateMachine val stateMachine = transientValues.stateMachine
val oldState = transientState val oldState = transientState
val actionExecutor = transientValues.actionExecutor val actionExecutor = transientValues.actionExecutor
val transition = stateMachine.transition(event, oldState) val transition = stateMachine.transition(event, oldState)
val (continuation, newState) = transitionExecutor.executeTransition(this, oldState, event, transition, actionExecutor) val (continuation, newState) = transitionExecutor.executeTransition(
// Ensure that the next state that is being written to the transient state maintains the [isKilled] flag this,
// This condition can be met if a flow is killed during [TransitionExecutor.executeTransition] oldState,
if (oldState.isKilled && !newState.isKilled) { event,
newState.isKilled = true transition,
} actionExecutor
)
transientState = newState transientState = newState
setLoggingContext() setLoggingContext()
return continuation continuation
}
} }
/** /**

View File

@ -3,6 +3,7 @@ package net.corda.node.services.statemachine
import co.paralleluniverse.fibers.Fiber import co.paralleluniverse.fibers.Fiber
import co.paralleluniverse.fibers.FiberExecutorScheduler import co.paralleluniverse.fibers.FiberExecutorScheduler
import co.paralleluniverse.fibers.instrument.JavaAgent import co.paralleluniverse.fibers.instrument.JavaAgent
import co.paralleluniverse.strands.channels.Channel
import com.codahale.metrics.Gauge import com.codahale.metrics.Gauge
import com.google.common.util.concurrent.ThreadFactoryBuilder import com.google.common.util.concurrent.ThreadFactoryBuilder
import net.corda.core.concurrent.CordaFuture import net.corda.core.concurrent.CordaFuture
@ -58,7 +59,6 @@ import javax.annotation.concurrent.ThreadSafe
import kotlin.collections.component1 import kotlin.collections.component1
import kotlin.collections.component2 import kotlin.collections.component2
import kotlin.collections.set import kotlin.collections.set
import kotlin.streams.toList
/** /**
* The StateMachineManagerImpl will always invoke the flow fibers on the given [AffinityExecutor], regardless of which * The StateMachineManagerImpl will always invoke the flow fibers on the given [AffinityExecutor], regardless of which
@ -77,6 +77,14 @@ internal class SingleThreadedStateMachineManager(
companion object { companion object {
private val logger = contextLogger() private val logger = contextLogger()
private val VALID_KILL_FLOW_STATUSES = setOf(
Checkpoint.FlowStatus.RUNNABLE,
Checkpoint.FlowStatus.FAILED,
Checkpoint.FlowStatus.COMPLETED,
Checkpoint.FlowStatus.HOSPITALIZED,
Checkpoint.FlowStatus.PAUSED
)
@VisibleForTesting @VisibleForTesting
var beforeClientIDCheck: (() -> Unit)? = null var beforeClientIDCheck: (() -> Unit)? = null
@VisibleForTesting @VisibleForTesting
@ -102,7 +110,7 @@ internal class SingleThreadedStateMachineManager(
private val flowTimeoutScheduler = FlowTimeoutScheduler(innerState, scheduledFutureExecutor, serviceHub) private val flowTimeoutScheduler = FlowTimeoutScheduler(innerState, scheduledFutureExecutor, serviceHub)
private val ourSenderUUID = serviceHub.networkService.ourSenderUUID private val ourSenderUUID = serviceHub.networkService.ourSenderUUID
private var checkpointSerializationContext: CheckpointSerializationContext? = null private lateinit var checkpointSerializationContext: CheckpointSerializationContext
private lateinit var flowCreator: FlowCreator private lateinit var flowCreator: FlowCreator
override val flowHospital: StaffedFlowHospital = makeFlowHospital() override val flowHospital: StaffedFlowHospital = makeFlowHospital()
@ -115,6 +123,26 @@ internal class SingleThreadedStateMachineManager(
private val totalStartedFlows = metrics.counter("Flows.Started") private val totalStartedFlows = metrics.counter("Flows.Started")
private val totalFinishedFlows = metrics.counter("Flows.Finished") private val totalFinishedFlows = metrics.counter("Flows.Finished")
private inline fun <R> Flow<R>.withFlowLock(
validStatuses: Set<Checkpoint.FlowStatus>,
block: FlowStateMachineImpl<R>.() -> Boolean
): Boolean {
if (!fiber.hasValidStatus(validStatuses)) return false
return fiber.withFlowLock {
// Get the flow again, in case another thread removed it from the map
innerState.withLock {
flows[id]?.run {
if (!fiber.hasValidStatus(validStatuses)) return false
block(uncheckedCast(this.fiber))
}
} ?: false
}
}
private fun FlowStateMachineImpl<*>.hasValidStatus(validStatuses: Set<Checkpoint.FlowStatus>): Boolean {
return transientState.checkpoint.status in validStatuses
}
/** /**
* An observable that emits triples of the changing flow, the type of change, and a process-specific ID number * An observable that emits triples of the changing flow, the type of change, and a process-specific ID number
* which may change across restarts. * which may change across restarts.
@ -153,12 +181,11 @@ internal class SingleThreadedStateMachineManager(
flowTimeoutScheduler::resetCustomTimeout flowTimeoutScheduler::resetCustomTimeout
) )
val fibers = restoreFlowsFromCheckpoints() val (fibers, pausedFlows) = restoreFlowsFromCheckpoints()
metrics.register("Flows.InFlight", Gauge<Int> { innerState.flows.size }) metrics.register("Flows.InFlight", Gauge<Int> { innerState.flows.size })
setFlowDefaultUncaughtExceptionHandler() setFlowDefaultUncaughtExceptionHandler()
val pausedFlows = restoreNonResidentFlowsFromPausedCheckpoints()
innerState.withLock { innerState.withLock {
this.pausedFlows.putAll(pausedFlows) this.pausedFlows.putAll(pausedFlows)
for ((id, flow) in pausedFlows) { for ((id, flow) in pausedFlows) {
@ -322,9 +349,9 @@ internal class SingleThreadedStateMachineManager(
} }
override fun killFlow(id: StateMachineRunId): Boolean { override fun killFlow(id: StateMachineRunId): Boolean {
val killFlowResult = innerState.withLock { val flow = innerState.withLock { flows[id] }
val flow = flows[id] val killFlowResult = if (flow != null) {
if (flow != null) { flow.withFlowLock(VALID_KILL_FLOW_STATUSES) {
logger.info("Killing flow $id known to this node.") logger.info("Killing flow $id known to this node.")
// The checkpoint and soft locks are removed here instead of relying on the processing of the next event after setting // The checkpoint and soft locks are removed here instead of relying on the processing of the next event after setting
// the killed flag. This is to ensure a flow can be removed from the database, even if it is stuck in a infinite loop. // the killed flag. This is to ensure a flow can be removed from the database, even if it is stuck in a infinite loop.
@ -332,24 +359,19 @@ internal class SingleThreadedStateMachineManager(
checkpointStorage.removeCheckpoint(id, mayHavePersistentResults = true) checkpointStorage.removeCheckpoint(id, mayHavePersistentResults = true)
serviceHub.vaultService.softLockRelease(id.uuid) serviceHub.vaultService.softLockRelease(id.uuid)
} }
// the same code is NOT done in remove flow when an error occurs
// what is the point of this latch?
unfinishedFibers.countDown() unfinishedFibers.countDown()
val state = flow.fiber.transientState flow.fiber.transientState = flow.fiber.transientState.copy(isKilled = true)
state.isKilled = true scheduleEvent(Event.DoRemainingWork)
flow.fiber.scheduleEvent(Event.DoRemainingWork)
true true
} else { } else {
// It may be that the id refers to a checkpoint that couldn't be deserialised into a flow, so we delete it if it exists. // It may be that the id refers to a checkpoint that couldn't be deserialised into a flow, so we delete it if it exists.
database.transaction { checkpointStorage.removeCheckpoint(id, mayHavePersistentResults = true) } database.transaction { checkpointStorage.removeCheckpoint(id, mayHavePersistentResults = true) }
} }
} }
return if (killFlowResult) {
true return killFlowResult || flowHospital.dropSessionInit(id)
} else {
flowHospital.dropSessionInit(id)
}
} }
private fun markAllFlowsAsPaused() { private fun markAllFlowsAsPaused() {
@ -425,38 +447,39 @@ internal class SingleThreadedStateMachineManager(
liveFibers.countUp() liveFibers.countUp()
} }
private fun restoreFlowsFromCheckpoints(): List<Flow<*>> { private fun restoreFlowsFromCheckpoints(): Pair<MutableMap<StateMachineRunId, Flow<*>>, MutableMap<StateMachineRunId, NonResidentFlow>> {
return checkpointStorage.getCheckpointsToRun().use { val flows = mutableMapOf<StateMachineRunId, Flow<*>>()
it.mapNotNull { (id, serializedCheckpoint) -> val pausedFlows = mutableMapOf<StateMachineRunId, NonResidentFlow>()
checkpointStorage.getCheckpointsToRun().forEach Checkpoints@{(id, serializedCheckpoint) ->
// If a flow is added before start() then don't attempt to restore it // If a flow is added before start() then don't attempt to restore it
innerState.withLock { if (id in flows) return@mapNotNull null } innerState.withLock { if (id in flows) return@Checkpoints }
val checkpoint = tryDeserializeCheckpoint(serializedCheckpoint, id)?.also { val checkpoint = tryDeserializeCheckpoint(serializedCheckpoint, id)?.also {
if (it.status == Checkpoint.FlowStatus.HOSPITALIZED) { if (it.status == Checkpoint.FlowStatus.HOSPITALIZED) {
checkpointStorage.updateStatus(id, Checkpoint.FlowStatus.RUNNABLE) checkpointStorage.updateStatus(id, Checkpoint.FlowStatus.RUNNABLE)
if (!checkpointStorage.removeFlowException(id)) { if (!checkpointStorage.removeFlowException(id)) {
logger.error("Unable to remove database exception for flow $id. Something is very wrong. The flow will not be loaded and run.") logger.error("Unable to remove database exception for flow $id. Something is very wrong. The flow will not be loaded and run.")
return@mapNotNull null return@Checkpoints
} }
} }
} ?: return@mapNotNull null } ?: return@Checkpoints
flowCreator.createFlowFromCheckpoint(id, checkpoint) val flow = flowCreator.createFlowFromCheckpoint(id, checkpoint)
}.toList() if (flow == null) {
// Set the flowState to paused so we don't waste memory storing it anymore.
pausedFlows[id] = NonResidentFlow(id, checkpoint.copy(flowState = FlowState.Paused), resumable = false)
} else {
flows[id] = flow
} }
} }
checkpointStorage.getPausedCheckpoints().forEach Checkpoints@{ (id, serializedCheckpoint) ->
val checkpoint = tryDeserializeCheckpoint(serializedCheckpoint, id) ?: return@Checkpoints
pausedFlows[id] = NonResidentFlow(id, checkpoint)
}
return Pair(flows, pausedFlows)
}
private fun restoreNonResidentFlowsFromPausedCheckpoints(): Map<StateMachineRunId, NonResidentFlow> { private fun resumeRestoredFlows(flows: Map<StateMachineRunId, Flow<*>>) {
return checkpointStorage.getPausedCheckpoints().use { for ((id, flow) in flows.entries) {
it.mapNotNull { (id, serializedCheckpoint) -> addAndStartFlow(id, flow)
// If a flow is added before start() then don't attempt to restore it
val checkpoint = tryDeserializeCheckpoint(serializedCheckpoint, id) ?: return@mapNotNull null
id to NonResidentFlow(id, checkpoint)
}.toList().toMap()
}
}
private fun resumeRestoredFlows(flows: List<Flow<*>>) {
for (flow in flows) {
addAndStartFlow(flow.fiber.id, flow)
} }
} }
@ -492,8 +515,13 @@ internal class SingleThreadedStateMachineManager(
} ?: return } ?: return
// Resurrect flow // Resurrect flow
flowCreator.createFlowFromCheckpoint(flowId, checkpoint, reloadCheckpointAfterSuspendCount = currentState.reloadCheckpointAfterSuspendCount) flowCreator.createFlowFromCheckpoint(
?: return flowId,
checkpoint,
currentState.reloadCheckpointAfterSuspendCount,
currentState.lock,
firstRestore = false
) ?: return
} else { } else {
// Just flow initiation message // Just flow initiation message
null null
@ -510,17 +538,56 @@ internal class SingleThreadedStateMachineManager(
injectOldProgressTracker(currentState.flowLogic.progressTracker, flow.fiber.logic) injectOldProgressTracker(currentState.flowLogic.progressTracker, flow.fiber.logic)
addAndStartFlow(flowId, flow) addAndStartFlow(flowId, flow)
} }
// Deliver all the external events from the old flow instance. extractAndScheduleEventsForRetry(oldFlowLeftOver, currentState)
val unprocessedExternalEvents = mutableListOf<ExternalEvent>() }
}
/**
* Extract all the [ExternalEvent] from this flows event queue and queue them (in the correct order) in the PausedFlow.
* This differs from [extractAndScheduleEventsForRetry] which also extracts (and schedules) [Event.Pause]. This means that if there are
* more events in the flows eventQueue then the flow won't pause again (after it is retried). These events are then scheduled (along
* with any [ExistingSessionMessage] which arrive in the interim) when the flow is retried.
*/
private fun extractAndQueueExternalEventsForPausedFlow(
currentEventQueue: Channel<Event>,
currentPendingDeduplicationHandlers: List<DeduplicationHandler>,
pausedFlow: NonResidentFlow
) {
pausedFlow.events += currentPendingDeduplicationHandlers.map{it.externalCause}
do { do {
val event = oldFlowLeftOver.tryReceive() val event = currentEventQueue.tryReceive()
if (event is Event.GeneratedByExternalEvent) { if (event is Event.GeneratedByExternalEvent) {
unprocessedExternalEvents += event.deduplicationHandler.externalCause pausedFlow.events.add(event.deduplicationHandler.externalCause)
} }
} while (event != null) } while (event != null)
val externalEvents = currentState.pendingDeduplicationHandlers.map { it.externalCause } + unprocessedExternalEvents }
for (externalEvent in externalEvents) {
deliverExternalEvent(externalEvent)
/**
* Extract all the incomplete deduplication handlers as well as the [ExternalEvent] and [Event.Pause] events from this flows event queue
* [oldEventQueue]. Then schedule them (in the same order) for the new flow. This means that if a retried flow has a pause event
* scheduled then the retried flow will eventually pause. The new flow will not retry again if future retry events have been scheduled.
* When this method is called this flow must have been replaced by the new flow in [StateMachineInnerState.flows]. This method differs
* from [extractAndQueueExternalEventsForPausedFlow] where (only) [externalEvents] are extracted and scheduled straight away.
*/
private fun extractAndScheduleEventsForRetry(oldEventQueue: Channel<Event>, currentState: StateMachineState) {
val flow = innerState.withLock {
flows[currentState.flowLogic.runId]
}
val events = mutableListOf<Event>()
do {
val event = oldEventQueue.tryReceive()
if (event is Event.Pause || event is Event.GeneratedByExternalEvent) events.add(event)
} while (event != null)
for (externalEvent in currentState.pendingDeduplicationHandlers) {
deliverExternalEvent(externalEvent.externalCause)
}
for (event in events) {
if (event is Event.GeneratedByExternalEvent) {
deliverExternalEvent(event.deduplicationHandler.externalCause)
} else {
flow?.fiber?.scheduleEvent(event)
} }
} }
} }
@ -559,7 +626,7 @@ internal class SingleThreadedStateMachineManager(
val sender = serviceHub.networkMapCache.getPeerByLegalName(peer) val sender = serviceHub.networkMapCache.getPeerByLegalName(peer)
if (sender != null) { if (sender != null) {
when (sessionMessage) { when (sessionMessage) {
is ExistingSessionMessage -> onExistingSessionMessage(sessionMessage, event.deduplicationHandler, sender) is ExistingSessionMessage -> onExistingSessionMessage(sessionMessage, sender, event)
is InitialSessionMessage -> onSessionInit(sessionMessage, sender, event) is InitialSessionMessage -> onSessionInit(sessionMessage, sender, event)
} }
} else { } else {
@ -569,8 +636,13 @@ internal class SingleThreadedStateMachineManager(
} }
} }
private fun onExistingSessionMessage(sessionMessage: ExistingSessionMessage, deduplicationHandler: DeduplicationHandler, sender: Party) { private fun onExistingSessionMessage(
sessionMessage: ExistingSessionMessage,
sender: Party,
externalEvent: ExternalEvent.ExternalMessageEvent
) {
try { try {
val deduplicationHandler = externalEvent.deduplicationHandler
val recipientId = sessionMessage.recipientSessionId val recipientId = sessionMessage.recipientSessionId
val flowId = sessionToFlow[recipientId] val flowId = sessionToFlow[recipientId]
if (flowId == null) { if (flowId == null) {
@ -589,7 +661,7 @@ internal class SingleThreadedStateMachineManager(
innerState.withLock { innerState.withLock {
flows[flowId]?.run { fiber.scheduleEvent(event) } flows[flowId]?.run { fiber.scheduleEvent(event) }
// If flow is not running add it to the list of external events to be processed if/when the flow resumes. // If flow is not running add it to the list of external events to be processed if/when the flow resumes.
?: pausedFlows[flowId]?.run { addExternalEvent(event) } ?: pausedFlows[flowId]?.run { addExternalEvent(externalEvent) }
?: logger.info("Cannot find fiber corresponding to flow ID $flowId") ?: logger.info("Cannot find fiber corresponding to flow ID $flowId")
} }
} }
@ -699,7 +771,16 @@ internal class SingleThreadedStateMachineManager(
null null
} }
val flow = flowCreator.createFlowFromLogic(flowId, invocationContext, flowLogic, flowStart, ourIdentity, existingCheckpoint, deduplicationHandler, ourSenderUUID) val flow = flowCreator.createFlowFromLogic(
flowId,
invocationContext,
flowLogic,
flowStart,
ourIdentity,
existingCheckpoint,
deduplicationHandler,
ourSenderUUID
)
val startedFuture = openFuture<Unit>() val startedFuture = openFuture<Unit>()
innerState.withLock { innerState.withLock {
startedFutures[flowId] = startedFuture startedFutures[flowId] = startedFuture
@ -717,9 +798,29 @@ internal class SingleThreadedStateMachineManager(
flowTimeoutScheduler.cancel(flowId) flowTimeoutScheduler.cancel(flowId)
} }
override fun moveFlowToPaused(currentState: StateMachineState) {
currentState.cancelFutureIfRunning()
flowTimeoutScheduler.cancel(currentState.flowLogic.runId)
innerState.withLock {
val id = currentState.flowLogic.runId
val flow = flows.remove(id)
if (flow != null) {
decrementLiveFibers()
//Setting flowState = FlowState.Paused means we don't hold the frozen fiber in memory.
val checkpoint = currentState.checkpoint.copy(status = Checkpoint.FlowStatus.PAUSED, flowState = FlowState.Paused)
val pausedFlow = NonResidentFlow(id, checkpoint, flow.resultFuture)
val eventQueue = flow.fiber.transientValues.eventQueue
extractAndQueueExternalEventsForPausedFlow(eventQueue, currentState.pendingDeduplicationHandlers, pausedFlow)
pausedFlows.put(id, pausedFlow)
} else {
logger.warn("Flow $id already removed before pausing")
}
}
}
private fun tryDeserializeCheckpoint(serializedCheckpoint: Checkpoint.Serialized, flowId: StateMachineRunId): Checkpoint? { private fun tryDeserializeCheckpoint(serializedCheckpoint: Checkpoint.Serialized, flowId: StateMachineRunId): Checkpoint? {
return try { return try {
serializedCheckpoint.deserialize(checkpointSerializationContext!!) serializedCheckpoint.deserialize(checkpointSerializationContext)
} catch (e: Exception) { } catch (e: Exception) {
if (reloadCheckpointAfterSuspend && currentStateMachine() != null) { if (reloadCheckpointAfterSuspend && currentStateMachine() != null) {
logger.error( logger.error(

View File

@ -104,6 +104,16 @@ class StaffedFlowHospital(private val flowMessaging: FlowMessaging,
*/ */
private val flowsInHospital = ConcurrentHashMap<StateMachineRunId, FlowFiber>() private val flowsInHospital = ConcurrentHashMap<StateMachineRunId, FlowFiber>()
/**
* Returns true if the flow is currently being treated in the hospital.
* The differs to flows with a medical history (which can accessed via [StaffedFlowHospital.contains]).
*/
@VisibleForTesting
internal fun flowInHospital(runId: StateMachineRunId): Boolean {
// The .keys avoids https://youtrack.jetbrains.com/issue/KT-18053
return runId in flowsInHospital.keys
}
private val mutex = ThreadBox(object { private val mutex = ThreadBox(object {
/** /**
* Contains medical history of every flow (a patient) that has entered the hospital. A flow can leave the hospital, * Contains medical history of every flow (a patient) that has entered the hospital. A flow can leave the hospital,

View File

@ -129,6 +129,7 @@ internal interface StateMachineManagerInternal {
fun addSessionBinding(flowId: StateMachineRunId, sessionId: SessionId) fun addSessionBinding(flowId: StateMachineRunId, sessionId: SessionId)
fun removeSessionBindings(sessionIds: Set<SessionId>) fun removeSessionBindings(sessionIds: Set<SessionId>)
fun removeFlow(flowId: StateMachineRunId, removalReason: FlowRemovalReason, lastState: StateMachineState) fun removeFlow(flowId: StateMachineRunId, removalReason: FlowRemovalReason, lastState: StateMachineState)
fun moveFlowToPaused(currentState: StateMachineState)
fun retryFlowFromSafePoint(currentState: StateMachineState) fun retryFlowFromSafePoint(currentState: StateMachineState)
fun scheduleFlowTimeout(flowId: StateMachineRunId) fun scheduleFlowTimeout(flowId: StateMachineRunId)
fun cancelFlowTimeout(flowId: StateMachineRunId) fun cancelFlowTimeout(flowId: StateMachineRunId)

View File

@ -25,6 +25,7 @@ import net.corda.node.services.messaging.DeduplicationHandler
import java.lang.IllegalStateException import java.lang.IllegalStateException
import java.time.Instant import java.time.Instant
import java.util.concurrent.Future import java.util.concurrent.Future
import java.util.concurrent.Semaphore
/** /**
* The state of the state machine, capturing the state of a flow. It consists of two parts, an *immutable* part that is * The state of the state machine, capturing the state of a flow. It consists of two parts, an *immutable* part that is
@ -44,9 +45,12 @@ import java.util.concurrent.Future
* @param isRemoved true if the flow has been removed from the state machine manager. This is used to avoid any further * @param isRemoved true if the flow has been removed from the state machine manager. This is used to avoid any further
* work. * work.
* @param isKilled true if the flow has been marked as killed. This is used to cause a flow to move to a killed flow transition no matter * @param isKilled true if the flow has been marked as killed. This is used to cause a flow to move to a killed flow transition no matter
* what event it is set to process next. [isKilled] is a `var` and set as [Volatile] to prevent concurrency errors that can occur if a flow * what event it is set to process next.
* is killed during the middle of a state transition.
* @param senderUUID the identifier of the sending state machine or null if this flow is resumed from a checkpoint so that it does not participate in de-duplication high-water-marking. * @param senderUUID the identifier of the sending state machine or null if this flow is resumed from a checkpoint so that it does not participate in de-duplication high-water-marking.
* @param reloadCheckpointAfterSuspendCount The number of times a flow has been reloaded (not retried). This is [null] when
* [NodeConfiguration.reloadCheckpointAfterSuspendCount] is not enabled.
* @param lock The flow's lock, used to prevent the flow performing a transition while being interacted with from external threads, and
* vise-versa.
*/ */
// TODO perhaps add a read-only environment to the state machine for things that don't change over time? // TODO perhaps add a read-only environment to the state machine for things that don't change over time?
// TODO evaluate persistent datastructure libraries to replace the inefficient copying we currently do. // TODO evaluate persistent datastructure libraries to replace the inefficient copying we currently do.
@ -60,10 +64,10 @@ data class StateMachineState(
val isAnyCheckpointPersisted: Boolean, val isAnyCheckpointPersisted: Boolean,
val isStartIdempotent: Boolean, val isStartIdempotent: Boolean,
val isRemoved: Boolean, val isRemoved: Boolean,
@Volatile val isKilled: Boolean,
var isKilled: Boolean,
val senderUUID: String?, val senderUUID: String?,
val reloadCheckpointAfterSuspendCount: Int? val reloadCheckpointAfterSuspendCount: Int?,
val lock: Semaphore
) : KryoSerializable { ) : KryoSerializable {
override fun write(kryo: Kryo?, output: Output?) { override fun write(kryo: Kryo?, output: Output?) {
throw IllegalStateException("${StateMachineState::class.qualifiedName} should never be serialized") throw IllegalStateException("${StateMachineState::class.qualifiedName} should never be serialized")

View File

@ -41,12 +41,7 @@ class StartedFlowTransition(
continuation = FlowContinuation.Throw(errorsToThrow[0]) continuation = FlowContinuation.Throw(errorsToThrow[0])
) )
} }
val sessionsToBeTerminated = findSessionsToBeTerminated(startingState) return when (flowIORequest) {
// if there are sessions to be closed, we close them as part of this transition and normal processing will continue on the next transition.
return if (sessionsToBeTerminated.isNotEmpty()) {
terminateSessions(sessionsToBeTerminated)
} else {
when (flowIORequest) {
is FlowIORequest.Send -> sendTransition(flowIORequest) is FlowIORequest.Send -> sendTransition(flowIORequest)
is FlowIORequest.Receive -> receiveTransition(flowIORequest) is FlowIORequest.Receive -> receiveTransition(flowIORequest)
is FlowIORequest.SendAndReceive -> sendAndReceiveTransition(flowIORequest) is FlowIORequest.SendAndReceive -> sendAndReceiveTransition(flowIORequest)
@ -57,31 +52,7 @@ class StartedFlowTransition(
is FlowIORequest.WaitForSessionConfirmations -> waitForSessionConfirmationsTransition() is FlowIORequest.WaitForSessionConfirmations -> waitForSessionConfirmationsTransition()
is FlowIORequest.ExecuteAsyncOperation<*> -> executeAsyncOperation(flowIORequest) is FlowIORequest.ExecuteAsyncOperation<*> -> executeAsyncOperation(flowIORequest)
FlowIORequest.ForceCheckpoint -> executeForceCheckpoint() FlowIORequest.ForceCheckpoint -> executeForceCheckpoint()
} }.let { scheduleTerminateSessionsIfRequired(it) }
}
}
private fun findSessionsToBeTerminated(startingState: StateMachineState): SessionMap {
return startingState.checkpoint.checkpointState.sessionsToBeClosed.mapNotNull { sessionId ->
val sessionState = startingState.checkpoint.checkpointState.sessions[sessionId]!! as SessionState.Initiated
if (sessionState.receivedMessages.isNotEmpty() && sessionState.receivedMessages.first() is EndSessionMessage) {
sessionId to sessionState
} else {
null
}
}.toMap()
}
private fun terminateSessions(sessionsToBeTerminated: SessionMap): TransitionResult {
return builder {
val sessionsToRemove = sessionsToBeTerminated.keys
val newCheckpoint = currentState.checkpoint.removeSessions(sessionsToRemove)
.removeSessionsToBeClosed(sessionsToRemove)
currentState = currentState.copy(checkpoint = newCheckpoint)
actions.add(Action.RemoveSessionBindings(sessionsToRemove))
actions.add(Action.ScheduleEvent(Event.DoRemainingWork))
FlowContinuation.ProcessEvents
}
} }
private fun waitForSessionConfirmationsTransition(): TransitionResult { private fun waitForSessionConfirmationsTransition(): TransitionResult {
@ -158,6 +129,7 @@ class StartedFlowTransition(
} }
} }
@Suppress("TooGenericExceptionCaught")
private fun sendAndReceiveTransition(flowIORequest: FlowIORequest.SendAndReceive): TransitionResult { private fun sendAndReceiveTransition(flowIORequest: FlowIORequest.SendAndReceive): TransitionResult {
val sessionIdToMessage = LinkedHashMap<SessionId, SerializedBytes<Any>>() val sessionIdToMessage = LinkedHashMap<SessionId, SerializedBytes<Any>>()
val sessionIdToSession = LinkedHashMap<SessionId, FlowSessionImpl>() val sessionIdToSession = LinkedHashMap<SessionId, FlowSessionImpl>()
@ -171,6 +143,7 @@ class StartedFlowTransition(
if (isErrored()) { if (isErrored()) {
FlowContinuation.ProcessEvents FlowContinuation.ProcessEvents
} else { } else {
try {
val receivedMap = receiveFromSessionsTransition(sessionIdToSession) val receivedMap = receiveFromSessionsTransition(sessionIdToSession)
if (receivedMap == null) { if (receivedMap == null) {
// We don't yet have the messages, change the suspension to be on Receive // We don't yet have the messages, change the suspension to be on Receive
@ -184,6 +157,10 @@ class StartedFlowTransition(
} else { } else {
resumeFlowLogic(receivedMap) resumeFlowLogic(receivedMap)
} }
} catch (t: Throwable) {
// E.g. A session end message received while expecting a data session message
resumeFlowLogic(t)
}
} }
} }
} }
@ -216,6 +193,7 @@ class StartedFlowTransition(
} }
} }
@Suppress("TooGenericExceptionCaught")
private fun receiveTransition(flowIORequest: FlowIORequest.Receive): TransitionResult { private fun receiveTransition(flowIORequest: FlowIORequest.Receive): TransitionResult {
return builder { return builder {
val sessionIdToSession = LinkedHashMap<SessionId, FlowSessionImpl>() val sessionIdToSession = LinkedHashMap<SessionId, FlowSessionImpl>()
@ -224,12 +202,17 @@ class StartedFlowTransition(
} }
// send initialises to uninitialised sessions // send initialises to uninitialised sessions
sendInitialSessionMessagesIfNeeded(sessionIdToSession.keys) sendInitialSessionMessagesIfNeeded(sessionIdToSession.keys)
try {
val receivedMap = receiveFromSessionsTransition(sessionIdToSession) val receivedMap = receiveFromSessionsTransition(sessionIdToSession)
if (receivedMap == null) { if (receivedMap == null) {
FlowContinuation.ProcessEvents FlowContinuation.ProcessEvents
} else { } else {
resumeFlowLogic(receivedMap) resumeFlowLogic(receivedMap)
} }
} catch (t: Throwable) {
// E.g. A session end message received while expecting a data session message
resumeFlowLogic(t)
}
} }
} }
@ -253,6 +236,8 @@ class StartedFlowTransition(
val messages: Map<SessionId, SerializedBytes<Any>>, val messages: Map<SessionId, SerializedBytes<Any>>,
val newSessionMap: SessionMap val newSessionMap: SessionMap
) )
@Suppress("ComplexMethod", "NestedBlockDepth")
private fun pollSessionMessages(sessions: SessionMap, sessionIds: Set<SessionId>): PollResult? { private fun pollSessionMessages(sessions: SessionMap, sessionIds: Set<SessionId>): PollResult? {
val newSessionMessages = LinkedHashMap(sessions) val newSessionMessages = LinkedHashMap(sessions)
val resultMessages = LinkedHashMap<SessionId, SerializedBytes<Any>>() val resultMessages = LinkedHashMap<SessionId, SerializedBytes<Any>>()
@ -267,7 +252,11 @@ class StartedFlowTransition(
} else { } else {
newSessionMessages[sessionId] = sessionState.copy(receivedMessages = messages.subList(1, messages.size).toList()) newSessionMessages[sessionId] = sessionState.copy(receivedMessages = messages.subList(1, messages.size).toList())
// at this point, we've already checked for errors and session ends, so it's guaranteed that the first message will be a data message. // at this point, we've already checked for errors and session ends, so it's guaranteed that the first message will be a data message.
resultMessages[sessionId] = (messages[0] as DataSessionMessage).payload resultMessages[sessionId] = if (messages[0] is EndSessionMessage) {
throw UnexpectedFlowEndException("Received session end message instead of a data session message. Mismatched send and receive?")
} else {
(messages[0] as DataSessionMessage).payload
}
} }
} }
else -> { else -> {
@ -537,4 +526,25 @@ class StartedFlowTransition(
private fun executeForceCheckpoint(): TransitionResult { private fun executeForceCheckpoint(): TransitionResult {
return builder { resumeFlowLogic(Unit) } return builder { resumeFlowLogic(Unit) }
} }
private fun scheduleTerminateSessionsIfRequired(transition: TransitionResult): TransitionResult {
// If there are sessions to be closed, close them on a following transition
val sessionsToBeTerminated = findSessionsToBeTerminated(transition.newState)
return if (sessionsToBeTerminated.isNotEmpty()) {
transition.copy(actions = transition.actions + Action.ScheduleEvent(Event.TerminateSessions(sessionsToBeTerminated.keys)))
} else {
transition
}
}
private fun findSessionsToBeTerminated(startingState: StateMachineState): SessionMap {
return startingState.checkpoint.checkpointState.sessionsToBeClosed.mapNotNull { sessionId ->
val sessionState = startingState.checkpoint.checkpointState.sessions[sessionId]!! as SessionState.Initiated
if (sessionState.receivedMessages.isNotEmpty() && sessionState.receivedMessages.first() is EndSessionMessage) {
sessionId to sessionState
} else {
null
}
}.toMap()
}
} }

View File

@ -62,6 +62,8 @@ class TopLevelTransition(
is Event.ReloadFlowFromCheckpointAfterSuspend -> reloadFlowFromCheckpointAfterSuspendTransition() is Event.ReloadFlowFromCheckpointAfterSuspend -> reloadFlowFromCheckpointAfterSuspendTransition()
is Event.OvernightObservation -> overnightObservationTransition() is Event.OvernightObservation -> overnightObservationTransition()
is Event.WakeUpFromSleep -> wakeUpFromSleepTransition() is Event.WakeUpFromSleep -> wakeUpFromSleepTransition()
is Event.Pause -> pausedFlowTransition()
is Event.TerminateSessions -> terminateSessionsTransition(event)
} }
} }
@ -378,4 +380,32 @@ class TopLevelTransition(
resumeFlowLogic(Unit) resumeFlowLogic(Unit)
} }
} }
private fun pausedFlowTransition(): TransitionResult {
return builder {
if (!startingState.isFlowResumed) {
actions.add(Action.CreateTransaction)
}
actions.addAll(
arrayOf(
Action.UpdateFlowStatus(context.id, Checkpoint.FlowStatus.PAUSED),
Action.CommitTransaction,
Action.MoveFlowToPaused(currentState)
)
)
FlowContinuation.Abort
}
}
private fun terminateSessionsTransition(event: Event.TerminateSessions): TransitionResult {
return builder {
val sessions = event.sessions
val newCheckpoint = currentState.checkpoint
.removeSessions(sessions)
.removeSessionsToBeClosed(sessions)
currentState = currentState.copy(checkpoint = newCheckpoint)
actions.add(Action.RemoveSessionBindings(sessions))
FlowContinuation.ProcessEvents
}
}
} }

View File

@ -1,7 +1,6 @@
additionalP2PAddresses = [] additionalP2PAddresses = []
crlCheckSoftFail = true crlCheckSoftFail = true
database = { database = {
transactionIsolationLevel = "REPEATABLE_READ"
exportHibernateJMXStatistics = "false" exportHibernateJMXStatistics = "false"
} }
dataSourceProperties = { dataSourceProperties = {

View File

@ -6,6 +6,8 @@ import net.corda.core.context.InvocationOrigin
import net.corda.core.contracts.ContractState import net.corda.core.contracts.ContractState
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.flows.StartableByService import net.corda.core.flows.StartableByService
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.packageName
import net.corda.core.node.AppServiceHub import net.corda.core.node.AppServiceHub
import net.corda.core.node.ServiceHub import net.corda.core.node.ServiceHub
import net.corda.core.node.services.CordaService import net.corda.core.node.services.CordaService
@ -16,12 +18,20 @@ import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.ProgressTracker
import net.corda.finance.DOLLARS import net.corda.finance.DOLLARS
import net.corda.finance.flows.CashIssueFlow import net.corda.finance.flows.CashIssueFlow
import net.corda.finance.schemas.CashSchemaV1
import net.corda.node.internal.cordapp.DummyRPCFlow import net.corda.node.internal.cordapp.DummyRPCFlow
import net.corda.testing.core.BOC_NAME
import net.corda.testing.core.DUMMY_NOTARY_NAME
import net.corda.testing.core.TestIdentity
import net.corda.testing.internal.vault.DummyLinearStateSchemaV1
import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNetwork
import net.corda.testing.node.MockNetworkParameters import net.corda.testing.node.MockNetworkParameters
import net.corda.testing.node.MockServices
import net.corda.testing.node.StartedMockNode import net.corda.testing.node.StartedMockNode
import net.corda.testing.node.internal.FINANCE_CONTRACTS_CORDAPP import net.corda.testing.node.internal.FINANCE_CONTRACTS_CORDAPP
import net.corda.testing.node.internal.enclosedCordapp import net.corda.testing.node.internal.enclosedCordapp
import net.corda.testing.node.makeTestIdentityService
import org.assertj.core.api.Assertions
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
@ -100,6 +110,22 @@ class CordaServiceTest {
nodeA.services.cordaService(EntityManagerService::class.java) nodeA.services.cordaService(EntityManagerService::class.java)
} }
@Test(timeout=300_000)
fun `MockServices when initialized with package name not on classpath throws ClassNotFoundException`() {
val cordappPackages = listOf(
"com.r3.corda.sdk.tokens.money",
"net.corda.finance.contracts",
CashSchemaV1::class.packageName,
DummyLinearStateSchemaV1::class.packageName)
val bankOfCorda = TestIdentity(BOC_NAME)
val dummyCashIssuer = TestIdentity(CordaX500Name("Snake Oil Issuer", "London", "GB"), 10)
val dummyNotary = TestIdentity(DUMMY_NOTARY_NAME, 20)
val identityService = makeTestIdentityService(dummyNotary.identity)
Assertions.assertThatThrownBy { MockServices(cordappPackages, dummyNotary, identityService, dummyCashIssuer.keyPair, bankOfCorda.keyPair) }
.isInstanceOf(ClassNotFoundException::class.java).hasMessage("Could not create jar file as the given package is not found on the classpath: com.r3.corda.sdk.tokens.money")
}
@StartableByService @StartableByService
class DummyServiceFlow : FlowLogic<InvocationContext>() { class DummyServiceFlow : FlowLogic<InvocationContext>() {
companion object { companion object {

View File

@ -280,7 +280,7 @@ class NodeConfigurationImplTest {
@Test(timeout=3_000) @Test(timeout=3_000)
fun `compatibilityZoneURL populates NetworkServices`() { fun `compatibilityZoneURL populates NetworkServices`() {
val compatibilityZoneURL = URI.create("https://r3.com").toURL() val compatibilityZoneURL = URI.create("https://r3.example.com").toURL()
val configuration = testConfiguration.copy( val configuration = testConfiguration.copy(
devMode = false, devMode = false,
compatibilityZoneURL = compatibilityZoneURL) compatibilityZoneURL = compatibilityZoneURL)

View File

@ -78,7 +78,6 @@ class NetworkMapUpdaterTest {
@Rule @Rule
@JvmField @JvmField
val testSerialization = SerializationEnvironmentRule(true) val testSerialization = SerializationEnvironmentRule(true)
private val cacheExpiryMs = 1000 private val cacheExpiryMs = 1000
private val privateNetUUID = UUID.randomUUID() private val privateNetUUID = UUID.randomUUID()
private val fs = Jimfs.newFileSystem(unix()) private val fs = Jimfs.newFileSystem(unix())
@ -120,12 +119,13 @@ class NetworkMapUpdaterTest {
networkParameters: NetworkParameters = server.networkParameters, networkParameters: NetworkParameters = server.networkParameters,
autoAcceptNetworkParameters: Boolean = true, autoAcceptNetworkParameters: Boolean = true,
excludedAutoAcceptNetworkParameters: Set<String> = emptySet()) { excludedAutoAcceptNetworkParameters: Set<String> = emptySet()) {
updater!!.start(DEV_ROOT_CA.certificate, updater!!.start(DEV_ROOT_CA.certificate,
server.networkParameters.serialize().hash, server.networkParameters.serialize().hash,
ourNodeInfo, ourNodeInfo,
networkParameters, networkParameters,
MockKeyManagementService(makeTestIdentityService(), ourKeyPair), MockKeyManagementService(makeTestIdentityService(), ourKeyPair),
NetworkParameterAcceptanceSettings(autoAcceptNetworkParameters, excludedAutoAcceptNetworkParameters)) NetworkParameterAcceptanceSettings(autoAcceptNetworkParameters, excludedAutoAcceptNetworkParameters), null)
} }
@Test(timeout=300_000) @Test(timeout=300_000)

View File

@ -0,0 +1,125 @@
package net.corda.node.services.network
import com.nhaarman.mockito_kotlin.any
import com.nhaarman.mockito_kotlin.never
import com.nhaarman.mockito_kotlin.verify
import net.corda.core.identity.Party
import net.corda.core.internal.NetworkParametersStorage
import net.corda.core.node.NetworkParameters
import net.corda.core.node.NotaryInfo
import net.corda.core.serialization.serialize
import net.corda.coretesting.internal.DEV_ROOT_CA
import net.corda.node.internal.NetworkParametersReader
import net.corda.nodeapi.internal.createDevNetworkMapCa
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
import net.corda.testing.common.internal.addNotary
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.core.SerializationEnvironmentRule
import net.corda.testing.core.TestIdentity
import org.junit.Assert
import org.junit.Rule
import org.junit.Test
import org.mockito.Mockito
class NetworkParametersHotloaderTest {
@Rule
@JvmField
val testSerialization = SerializationEnvironmentRule(true)
private val networkMapCertAndKeyPair: CertificateAndKeyPair = createDevNetworkMapCa()
private val trustRoot = DEV_ROOT_CA.certificate
private val originalNetworkParameters = testNetworkParameters()
private val notary: Party = TestIdentity.fresh("test notary").party
private val networkParametersWithNotary = originalNetworkParameters.addNotary(notary)
private val networkParametersStorage = Mockito.mock(NetworkParametersStorage::class.java)
@Test(timeout = 300_000)
fun `can hotload if notary changes`() {
`can hotload`(networkParametersWithNotary)
}
@Test(timeout = 300_000)
fun `can not hotload if notary changes but another non-hotloadable property also changes`() {
val newnetParamsWithNewNotaryAndMaxMsgSize = networkParametersWithNotary.copy(maxMessageSize = networkParametersWithNotary.maxMessageSize + 1)
`can not hotload`(newnetParamsWithNewNotaryAndMaxMsgSize)
}
@Test(timeout = 300_000)
fun `can hotload if only always hotloadable properties change`() {
val newParametersWithAlwaysHotloadableProperties = originalNetworkParameters.copy(epoch = originalNetworkParameters.epoch + 1, modifiedTime = originalNetworkParameters.modifiedTime.plusSeconds(60))
`can hotload`(newParametersWithAlwaysHotloadableProperties)
}
@Test(timeout = 300_000)
fun `can not hotload if maxMessageSize changes`() {
val parametersWithNewMaxMessageSize = originalNetworkParameters.copy(maxMessageSize = originalNetworkParameters.maxMessageSize + 1)
`can not hotload`(parametersWithNewMaxMessageSize)
}
@Test(timeout = 300_000)
fun `can not hotload if maxTransactionSize changes`() {
val parametersWithNewMaxTransactionSize = originalNetworkParameters.copy(maxTransactionSize = originalNetworkParameters.maxMessageSize + 1)
`can not hotload`(parametersWithNewMaxTransactionSize)
}
@Test(timeout = 300_000)
fun `can not hotload if minimumPlatformVersion changes`() {
val parametersWithNewMinimumPlatformVersion = originalNetworkParameters.copy(minimumPlatformVersion = originalNetworkParameters.minimumPlatformVersion + 1)
`can not hotload`(parametersWithNewMinimumPlatformVersion)
}
private fun `can hotload`(newNetworkParameters: NetworkParameters) {
val notaryUpdateListener = Mockito.spy(object : NotaryUpdateListener {
override fun onNewNotaryList(notaries: List<NotaryInfo>) {
}
})
val networkParametersChangedListener = Mockito.spy(object : NetworkParameterUpdateListener {
override fun onNewNetworkParameters(networkParameters: NetworkParameters) {
}
})
val networkParametersHotloader = createHotloaderWithMockedServices(newNetworkParameters).also {
it.addNotaryUpdateListener(notaryUpdateListener)
it.addNetworkParametersChangedListeners(networkParametersChangedListener)
}
Assert.assertTrue(networkParametersHotloader.attemptHotload(newNetworkParameters.serialize().hash))
verify(notaryUpdateListener).onNewNotaryList(newNetworkParameters.notaries)
verify(networkParametersChangedListener).onNewNetworkParameters(newNetworkParameters)
}
private fun `can not hotload`(newNetworkParameters: NetworkParameters) {
val notaryUpdateListener = Mockito.spy(object : NotaryUpdateListener {
override fun onNewNotaryList(notaries: List<NotaryInfo>) {
}
})
val networkParametersChangedListener = Mockito.spy(object : NetworkParameterUpdateListener {
override fun onNewNetworkParameters(networkParameters: NetworkParameters) {
}
})
val networkParametersHotloader = createHotloaderWithMockedServices(newNetworkParameters).also {
it.addNotaryUpdateListener(notaryUpdateListener)
it.addNetworkParametersChangedListeners(networkParametersChangedListener)
}
Assert.assertFalse(networkParametersHotloader.attemptHotload(newNetworkParameters.serialize().hash))
verify(notaryUpdateListener, never()).onNewNotaryList(any());
verify(networkParametersChangedListener, never()).onNewNetworkParameters(any());
}
private fun createHotloaderWithMockedServices(newNetworkParameters: NetworkParameters): NetworkParametersHotloader {
val signedNetworkParameters = networkMapCertAndKeyPair.sign(newNetworkParameters)
val networkMapClient = Mockito.mock(NetworkMapClient::class.java)
Mockito.`when`(networkMapClient.getNetworkParameters(newNetworkParameters.serialize().hash)).thenReturn(signedNetworkParameters)
val networkParametersReader = Mockito.mock(NetworkParametersReader::class.java)
Mockito.`when`(networkParametersReader.read())
.thenReturn(NetworkParametersReader.NetworkParametersAndSigned(signedNetworkParameters, trustRoot))
return NetworkParametersHotloader(networkMapClient, trustRoot, originalNetworkParameters, networkParametersReader, networkParametersStorage)
}
}

View File

@ -645,8 +645,8 @@ class DBCheckpointStorageTests {
val (extractedId, extractedCheckpoint) = checkpointStorage.getPausedCheckpoints().toList().single() val (extractedId, extractedCheckpoint) = checkpointStorage.getPausedCheckpoints().toList().single()
assertEquals(id, extractedId) assertEquals(id, extractedId)
//We don't extract the result or the flowstate from a paused checkpoint //We don't extract the result or the flowstate from a paused checkpoint
assertEquals(null, extractedCheckpoint.serializedFlowState) assertNull(extractedCheckpoint.serializedFlowState)
assertEquals(null, extractedCheckpoint.result) assertNull(extractedCheckpoint.result)
assertEquals(pausedCheckpoint.status, extractedCheckpoint.status) assertEquals(pausedCheckpoint.status, extractedCheckpoint.status)
assertEquals(pausedCheckpoint.progressStep, extractedCheckpoint.progressStep) assertEquals(pausedCheckpoint.progressStep, extractedCheckpoint.progressStep)
@ -736,6 +736,24 @@ class DBCheckpointStorageTests {
} }
} }
@Test(timeout = 300_000)
fun `update only compatible`() {
val (id, checkpoint) = newCheckpoint()
val serializedFlowState = checkpoint.serializeFlowState()
database.transaction {
checkpointStorage.addCheckpoint(id, checkpoint, serializedFlowState, checkpoint.serializeCheckpointState())
}
database.transaction {
checkpointStorage.updateCompatible(id, !checkpoint.compatible)
}
database.transaction {
assertEquals(
checkpoint.copy(compatible = !checkpoint.compatible),
checkpointStorage.checkpoints().single().deserialize()
)
}
}
@Test(timeout = 300_000) @Test(timeout = 300_000)
fun `'getFinishedFlowsResultsMetadata' fetches flows results metadata for finished flows only`() { fun `'getFinishedFlowsResultsMetadata' fetches flows results metadata for finished flows only`() {
val (_, checkpoint) = newCheckpoint(1) val (_, checkpoint) = newCheckpoint(1)

View File

@ -9,22 +9,22 @@ import net.corda.core.crypto.SignatureMetadata
import net.corda.core.crypto.TransactionSignature import net.corda.core.crypto.TransactionSignature
import net.corda.core.toFuture import net.corda.core.toFuture
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.node.services.transactions.PersistentUniquenessProvider
import net.corda.node.CordaClock import net.corda.node.CordaClock
import net.corda.node.MutableClock import net.corda.node.MutableClock
import net.corda.node.SimpleClock import net.corda.node.SimpleClock
import net.corda.node.services.transactions.PersistentUniquenessProvider
import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.testing.core.* import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.DUMMY_NOTARY_NAME
import net.corda.testing.core.SerializationEnvironmentRule
import net.corda.testing.core.TestIdentity
import net.corda.testing.core.dummyCommand
import net.corda.testing.internal.LogHelper import net.corda.testing.internal.LogHelper
import net.corda.testing.internal.TestingNamedCacheFactory import net.corda.testing.internal.TestingNamedCacheFactory
import net.corda.testing.internal.configureDatabase import net.corda.testing.internal.configureDatabase
import net.corda.testing.internal.createWireTransaction import net.corda.testing.internal.createWireTransaction
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
import org.apache.logging.log4j.LogManager
import org.apache.logging.log4j.core.Appender
import org.apache.logging.log4j.core.LoggerContext
import org.apache.logging.log4j.core.appender.WriterAppender
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.junit.After import org.junit.After
import org.junit.Assert import org.junit.Assert
@ -32,10 +32,9 @@ import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import rx.plugins.RxJavaHooks import rx.plugins.RxJavaHooks
import java.io.StringWriter
import java.util.concurrent.Semaphore
import java.time.Clock import java.time.Clock
import java.time.Instant import java.time.Instant
import java.util.concurrent.Semaphore
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.concurrent.thread import kotlin.concurrent.thread
import kotlin.test.assertEquals import kotlin.test.assertEquals
@ -381,47 +380,14 @@ class DBTransactionStorageTests {
val signedTransaction = newTransaction() val signedTransaction = newTransaction()
// Act // Act
val logMessages = collectLogsFrom { val warning = database.transaction {
database.transaction { val (result, warning) = transactionStorage.trackTransactionInternal(signedTransaction.id)
val result = transactionStorage.trackTransaction(signedTransaction.id)
result.cancel(false) result.cancel(false)
} warning
} }
// Assert // Assert
assertThat(logMessages).contains("trackTransaction is called with an already existing, open DB transaction. As a result, there might be transactions missing from the returned data feed, because of race conditions.") assertThat(warning).isEqualTo(DBTransactionStorage.TRANSACTION_ALREADY_IN_PROGRESS_WARNING)
}
private fun collectLogsFrom(statement: () -> Unit): String {
// Create test appender
val stringWriter = StringWriter()
val appenderName = this::collectLogsFrom.name
val appender: Appender = WriterAppender.createAppender(
null,
null,
stringWriter,
appenderName,
false,
true
)
appender.start()
// Add test appender
val context = LogManager.getContext(false) as LoggerContext
val configuration = context.configuration
configuration.addAppender(appender)
configuration.loggers.values.forEach { it.addAppender(appender, null, null) }
try {
statement()
} finally {
// Remove test appender
configuration.loggers.values.forEach { it.removeAppender(appenderName) }
configuration.appenders.remove(appenderName)
appender.stop()
}
return stringWriter.toString()
} }
private fun newTransactionStorage(cacheSizeBytesOverride: Long? = null, clock: CordaClock = SimpleClock(Clock.systemUTC())) { private fun newTransactionStorage(cacheSizeBytesOverride: Long? = null, clock: CordaClock = SimpleClock(Clock.systemUTC())) {

View File

@ -2,12 +2,13 @@ package net.corda.node.services.persistence
import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.MappedSchema
import net.corda.core.utilities.contextLogger import net.corda.core.utilities.contextLogger
import net.corda.node.internal.checkOrUpdate
import net.corda.node.internal.createCordaPersistence import net.corda.node.internal.createCordaPersistence
import net.corda.node.internal.startHikariPool import net.corda.node.internal.startHikariPool
import net.corda.node.services.schema.NodeSchemaService import net.corda.node.services.schema.NodeSchemaService
import net.corda.node.utilities.AppendOnlyPersistentMap import net.corda.node.utilities.AppendOnlyPersistentMap
import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.nodeapi.internal.persistence.TransactionIsolationLevel import net.corda.nodeapi.internal.persistence.SchemaMigration
import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.TestIdentity import net.corda.testing.core.TestIdentity
import net.corda.testing.internal.TestingNamedCacheFactory import net.corda.testing.internal.TestingNamedCacheFactory
@ -91,10 +92,13 @@ class DbMapDeadlockTest {
fun recreateDeadlock(hikariProperties: Properties) { fun recreateDeadlock(hikariProperties: Properties) {
val cacheFactory = TestingNamedCacheFactory() val cacheFactory = TestingNamedCacheFactory()
val dbConfig = DatabaseConfig(initialiseSchema = true, transactionIsolationLevel = TransactionIsolationLevel.READ_COMMITTED) val dbConfig = DatabaseConfig()
val schemaService = NodeSchemaService(extraSchemas = setOf(LockDbSchemaV2)) val schemaService = NodeSchemaService(extraSchemas = setOf(LockDbSchemaV2))
createCordaPersistence(dbConfig, { null }, { null }, schemaService, hikariProperties, cacheFactory, null).apply { createCordaPersistence(dbConfig, { null }, { null }, schemaService, hikariProperties, cacheFactory, null).apply {
startHikariPool(hikariProperties, dbConfig, schemaService.schemas, ourName = TestIdentity(ALICE_NAME, 70).name) startHikariPool(hikariProperties) { dataSource, haveCheckpoints ->
SchemaMigration(dataSource, null, null, TestIdentity(ALICE_NAME, 70).name)
.checkOrUpdate(schemaService.schemas, true, haveCheckpoints, false)
}
}.use { persistence -> }.use { persistence ->
// First clean up any remains from previous test runs // First clean up any remains from previous test runs

View File

@ -48,6 +48,7 @@ import net.corda.testing.internal.vault.VaultFiller
import net.corda.testing.node.MockServices import net.corda.testing.node.MockServices
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
import org.assertj.core.api.Assertions import org.assertj.core.api.Assertions
import org.assertj.core.api.Assertions.`in`
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy import org.assertj.core.api.Assertions.assertThatThrownBy
import org.hibernate.SessionFactory import org.hibernate.SessionFactory
@ -976,7 +977,7 @@ class HibernateConfigurationTest {
doReturn(it.party).whenever(mock).wellKnownPartyFromX500Name(it.name) doReturn(it.party).whenever(mock).wellKnownPartyFromX500Name(it.name)
} }
} }
database = configureDatabase(dataSourceProps, DatabaseConfig(initialiseSchema = initialiseSchema), identityService::wellKnownPartyFromX500Name, identityService::wellKnownPartyFromAnonymous, schemaService) database = configureDatabase(dataSourceProps, DatabaseConfig(), identityService::wellKnownPartyFromX500Name, identityService::wellKnownPartyFromAnonymous, schemaService, runMigrationScripts = initialiseSchema, allowHibernateToManageAppSchema = initialiseSchema)
return database return database
} }

View File

@ -9,6 +9,7 @@ import net.corda.core.flows.FlowSession
import net.corda.core.flows.InitiatedBy import net.corda.core.flows.InitiatedBy
import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.InitiatingFlow
import net.corda.core.flows.KilledFlowException import net.corda.core.flows.KilledFlowException
import net.corda.core.flows.UnexpectedFlowEndException
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.concurrent.flatMap import net.corda.core.internal.concurrent.flatMap
@ -21,7 +22,6 @@ import net.corda.node.services.FinalityHandler
import net.corda.node.services.messaging.Message import net.corda.node.services.messaging.Message
import net.corda.node.services.persistence.DBTransactionStorage import net.corda.node.services.persistence.DBTransactionStorage
import net.corda.nodeapi.internal.persistence.contextTransaction import net.corda.nodeapi.internal.persistence.contextTransaction
import net.corda.testing.common.internal.eventually
import net.corda.testing.core.TestIdentity import net.corda.testing.core.TestIdentity
import net.corda.testing.node.internal.InternalMockNetwork import net.corda.testing.node.internal.InternalMockNetwork
import net.corda.testing.node.internal.MessagingServiceSpy import net.corda.testing.node.internal.MessagingServiceSpy
@ -29,6 +29,7 @@ import net.corda.testing.node.internal.TestStartedNode
import net.corda.testing.node.internal.enclosedCordapp import net.corda.testing.node.internal.enclosedCordapp
import net.corda.testing.node.internal.newContext import net.corda.testing.node.internal.newContext
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatExceptionOfType
import org.h2.util.Utils import org.h2.util.Utils
import org.junit.After import org.junit.After
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
@ -38,7 +39,9 @@ import java.sql.SQLException
import java.time.Duration import java.time.Duration
import java.time.Instant import java.time.Instant
import java.util.Collections import java.util.Collections
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.CountDownLatch
import java.util.concurrent.Semaphore
import java.util.concurrent.TimeUnit
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertFailsWith import kotlin.test.assertFailsWith
import kotlin.test.assertNotNull import kotlin.test.assertNotNull
@ -58,7 +61,6 @@ class RetryFlowMockTest {
RetryFlow.count = 0 RetryFlow.count = 0
SendAndRetryFlow.count = 0 SendAndRetryFlow.count = 0
RetryInsertFlow.count = 0 RetryInsertFlow.count = 0
KeepSendingFlow.count.set(0)
StaffedFlowHospital.DatabaseEndocrinologist.customConditions.add { t -> t is LimitedRetryCausingError } StaffedFlowHospital.DatabaseEndocrinologist.customConditions.add { t -> t is LimitedRetryCausingError }
StaffedFlowHospital.DatabaseEndocrinologist.customConditions.add { t -> t is RetryCausingError } StaffedFlowHospital.DatabaseEndocrinologist.customConditions.add { t -> t is RetryCausingError }
} }
@ -99,34 +101,40 @@ class RetryFlowMockTest {
fun `Restart does not set senderUUID`() { fun `Restart does not set senderUUID`() {
val messagesSent = Collections.synchronizedList(mutableListOf<Message>()) val messagesSent = Collections.synchronizedList(mutableListOf<Message>())
val partyB = nodeB.info.legalIdentities.first() val partyB = nodeB.info.legalIdentities.first()
val expectedMessagesSent = CountDownLatch(3)
nodeA.setMessagingServiceSpy(object : MessagingServiceSpy() { nodeA.setMessagingServiceSpy(object : MessagingServiceSpy() {
override fun send(message: Message, target: MessageRecipients, sequenceKey: Any) { override fun send(message: Message, target: MessageRecipients, sequenceKey: Any) {
messagesSent.add(message) messagesSent.add(message)
expectedMessagesSent.countDown()
messagingService.send(message, target) messagingService.send(message, target)
} }
}) })
val count = 10000 // Lots of iterations so the flow keeps going long enough nodeA.startFlow(KeepSendingFlow(partyB))
nodeA.startFlow(KeepSendingFlow(count, partyB)) KeepSendingFlow.lock.acquire()
eventually(duration = Duration.ofSeconds(30), waitBetween = Duration.ofMillis(100)) {
assertTrue(messagesSent.isNotEmpty()) assertTrue(messagesSent.isNotEmpty())
assertNotNull(messagesSent.first().senderUUID) assertNotNull(messagesSent.first().senderUUID)
}
nodeA = mockNet.restartNode(nodeA) nodeA = mockNet.restartNode(nodeA)
// This is a bit racy because restarting the node actually starts it, so we need to make sure there's enough iterations we get here with flow still going.
nodeA.setMessagingServiceSpy(object : MessagingServiceSpy() { nodeA.setMessagingServiceSpy(object : MessagingServiceSpy() {
override fun send(message: Message, target: MessageRecipients, sequenceKey: Any) { override fun send(message: Message, target: MessageRecipients, sequenceKey: Any) {
messagesSent.add(message) messagesSent.add(message)
expectedMessagesSent.countDown()
messagingService.send(message, target) messagingService.send(message, target)
} }
}) })
// Now short circuit the iterations so the flow finishes soon. ReceiveFlow3.lock.release()
KeepSendingFlow.count.set(count - 2) assertTrue(expectedMessagesSent.await(20, TimeUnit.SECONDS))
eventually(duration = Duration.ofSeconds(30), waitBetween = Duration.ofMillis(100)) { assertEquals(3, messagesSent.size)
assertTrue(nodeA.smm.allStateMachines.isEmpty())
}
assertNull(messagesSent.last().senderUUID) assertNull(messagesSent.last().senderUUID)
} }
@Test(timeout=300_000)
fun `Early end session message does not hang receiving flow`() {
val partyB = nodeB.info.legalIdentities.first()
assertThatExceptionOfType(UnexpectedFlowEndException::class.java).isThrownBy {
nodeA.startFlow(UnbalancedSendAndReceiveFlow(partyB)).getOrThrow(20.seconds)
}.withMessage("Received session end message instead of a data session message. Mismatched send and receive?")
}
@Test(timeout=300_000) @Test(timeout=300_000)
fun `Retry duplicate insert`() { fun `Retry duplicate insert`() {
assertEquals(Unit, nodeA.startFlow(RetryInsertFlow(1)).get()) assertEquals(Unit, nodeA.startFlow(RetryInsertFlow(1)).get())
@ -252,32 +260,36 @@ class RetryFlowMockTest {
} }
@InitiatingFlow @InitiatingFlow
class KeepSendingFlow(private val i: Int, private val other: Party) : FlowLogic<Unit>() { class KeepSendingFlow(private val other: Party) : FlowLogic<Unit>() {
companion object { companion object {
val count = AtomicInteger(0) val lock = Semaphore(0)
} }
@Suspendable @Suspendable
override fun call() { override fun call() {
val session = initiateFlow(other) val session = initiateFlow(other)
session.send(i.toString()) session.send("boo")
do { lock.release()
logger.info("Sending... $count") session.receive<String>()
session.send("Boo") session.send("boo")
} while (count.getAndIncrement() < i)
} }
} }
@Suppress("unused") @Suppress("unused")
@InitiatedBy(KeepSendingFlow::class) @InitiatedBy(KeepSendingFlow::class)
class ReceiveFlow3(private val other: FlowSession) : FlowLogic<Unit>() { class ReceiveFlow3(private val other: FlowSession) : FlowLogic<Unit>() {
companion object {
val lock = Semaphore(0)
}
@Suspendable @Suspendable
override fun call() { override fun call() {
var count = other.receive<String>().unwrap { it.toInt() } other.receive<String>()
while (count-- > 0) { lock.acquire()
val received = other.receive<String>().unwrap { it } other.send("hoo")
logger.info("Received... $received $count") other.receive<String>()
}
} }
} }
@ -304,4 +316,27 @@ class RetryFlowMockTest {
contextTransaction.session.save(tx) contextTransaction.session.save(tx)
} }
} }
@InitiatingFlow
class UnbalancedSendAndReceiveFlow(private val other: Party) : FlowLogic<Unit>() {
@Suspendable
override fun call() {
val session = initiateFlow(other)
session.send("boo")
session.receive<String>()
session.receive<String>()
}
}
@Suppress("unused")
@InitiatedBy(UnbalancedSendAndReceiveFlow::class)
class UnbalancedSendAndReceiveResponder(private val other: FlowSession) : FlowLogic<Unit>() {
@Suspendable
override fun call() {
other.receive<String>()
other.send("hoo")
}
}
} }

View File

@ -12,7 +12,6 @@ dataSourceProperties = {
dataSource.password = "" dataSource.password = ""
} }
database = { database = {
transactionIsolationLevel = "REPEATABLE_READ"
exportHibernateJMXStatistics = "false" exportHibernateJMXStatistics = "false"
} }
p2pAddress = "localhost:2233" p2pAddress = "localhost:2233"

View File

@ -11,7 +11,6 @@ dataSourceProperties = {
dataSource.password = "" dataSource.password = ""
} }
database = { database = {
transactionIsolationLevel = "REPEATABLE_READ"
exportHibernateJMXStatistics = "false" exportHibernateJMXStatistics = "false"
} }
p2pAddress = "localhost:2233" p2pAddress = "localhost:2233"

View File

@ -12,7 +12,6 @@ dataSourceProperties = {
dataSource.password = "" dataSource.password = ""
} }
database = { database = {
transactionIsolationLevel = "REPEATABLE_READ"
exportHibernateJMXStatistics = "false" exportHibernateJMXStatistics = "false"
} }
p2pAddress = "localhost:2233" p2pAddress = "localhost:2233"

View File

@ -90,6 +90,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar', nodeTask,
} }
cordapp project(':samples:attachment-demo:contracts') cordapp project(':samples:attachment-demo:contracts')
cordapp project(':samples:attachment-demo:workflows') cordapp project(':samples:attachment-demo:workflows')
runSchemaMigration = true
} }
node { node {
name "O=Notary Node,L=Zurich,C=CH" name "O=Notary Node,L=Zurich,C=CH"

View File

@ -48,6 +48,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar', nodeTask,
nodeDefaults { nodeDefaults {
cordapp project(':finance:workflows') cordapp project(':finance:workflows')
cordapp project(':finance:contracts') cordapp project(':finance:contracts')
runSchemaMigration = true
} }
node { node {
name "O=Notary Node,L=Zurich,C=CH" name "O=Notary Node,L=Zurich,C=CH"

View File

@ -25,6 +25,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar', nodeTask,
} }
rpcUsers = [['username': "default", 'password': "default", 'permissions': [ 'ALL' ]]] rpcUsers = [['username': "default", 'password': "default", 'permissions': [ 'ALL' ]]]
cordapp project(':samples:cordapp-configuration:workflows') cordapp project(':samples:cordapp-configuration:workflows')
runSchemaMigration = true
} }
node { node {
name "O=Notary Node,L=Zurich,C=CH" name "O=Notary Node,L=Zurich,C=CH"

View File

@ -60,6 +60,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar', nodeTask])
} }
cordapp project(':samples:irs-demo:cordapp:contracts-irs') cordapp project(':samples:irs-demo:cordapp:contracts-irs')
cordapp project(':samples:irs-demo:cordapp:workflows-irs') cordapp project(':samples:irs-demo:cordapp:workflows-irs')
runSchemaMigration = true
} }
node { node {
name "O=Notary Node,L=Zurich,C=CH" name "O=Notary Node,L=Zurich,C=CH"

View File

@ -39,7 +39,9 @@ import org.junit.Test
import rx.Observable import rx.Observable
import java.time.Duration import java.time.Duration
import java.time.LocalDate import java.time.LocalDate
import org.junit.Ignore
@Ignore
class IRSDemoTest { class IRSDemoTest {
companion object { companion object {
private val log = contextLogger() private val log = contextLogger()

View File

@ -36,6 +36,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar', nodeTask])
} }
cordapp project(':samples:network-verifier:contracts') cordapp project(':samples:network-verifier:contracts')
cordapp project(':samples:network-verifier:workflows') cordapp project(':samples:network-verifier:workflows')
runSchemaMigration = true
} }
node { node {
name "O=Notary Node,L=Zurich,C=CH" name "O=Notary Node,L=Zurich,C=CH"

View File

@ -44,6 +44,7 @@ task deployNodesSingle(type: Cordform, dependsOn: ['jar', nodeTask, webTask]) {
extraConfig = [h2Settings: [address: "localhost:0"]] extraConfig = [h2Settings: [address: "localhost:0"]]
cordapp project(':samples:notary-demo:contracts') cordapp project(':samples:notary-demo:contracts')
cordapp project(':samples:notary-demo:workflows') cordapp project(':samples:notary-demo:workflows')
runSchemaMigration = true
} }
node { node {
name "O=Alice Corp,L=Madrid,C=ES" name "O=Alice Corp,L=Madrid,C=ES"

View File

@ -91,6 +91,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar', nodeTask,
cordapp project(':samples:simm-valuation-demo:contracts-states') cordapp project(':samples:simm-valuation-demo:contracts-states')
cordapp project(':samples:simm-valuation-demo:flows') cordapp project(':samples:simm-valuation-demo:flows')
rpcUsers = [['username': "default", 'password': "default", 'permissions': [ 'ALL' ]]] rpcUsers = [['username': "default", 'password': "default", 'permissions': [ 'ALL' ]]]
runSchemaMigration = true
} }
node { node {
name "O=Notary Node,L=Zurich,C=CH" name "O=Notary Node,L=Zurich,C=CH"

View File

@ -81,6 +81,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar', nodeTask])
cordapp project(':finance:workflows') cordapp project(':finance:workflows')
cordapp project(':finance:contracts') cordapp project(':finance:contracts')
cordapp project(':samples:trader-demo:workflows-trader') cordapp project(':samples:trader-demo:workflows-trader')
runSchemaMigration = true
} }
node { node {
name "O=Notary Node,L=Zurich,C=CH" name "O=Notary Node,L=Zurich,C=CH"

View File

@ -205,7 +205,8 @@ fun <A> driver(defaultParameters: DriverParameters = DriverParameters(), dsl: Dr
cordappsForAllNodes = uncheckedCast(defaultParameters.cordappsForAllNodes), cordappsForAllNodes = uncheckedCast(defaultParameters.cordappsForAllNodes),
djvmBootstrapSource = defaultParameters.djvmBootstrapSource, djvmBootstrapSource = defaultParameters.djvmBootstrapSource,
djvmCordaSource = defaultParameters.djvmCordaSource, djvmCordaSource = defaultParameters.djvmCordaSource,
environmentVariables = defaultParameters.environmentVariables environmentVariables = defaultParameters.environmentVariables,
allowHibernateToManageAppSchema = defaultParameters.allowHibernateToManageAppSchema
), ),
coerce = { it }, coerce = { it },
dsl = dsl dsl = dsl
@ -266,7 +267,8 @@ data class DriverParameters(
val cordappsForAllNodes: Collection<TestCordapp>? = null, val cordappsForAllNodes: Collection<TestCordapp>? = null,
val djvmBootstrapSource: Path? = null, val djvmBootstrapSource: Path? = null,
val djvmCordaSource: List<Path> = emptyList(), val djvmCordaSource: List<Path> = emptyList(),
val environmentVariables : Map<String, String> = emptyMap() val environmentVariables : Map<String, String> = emptyMap(),
val allowHibernateToManageAppSchema: Boolean = true
) { ) {
constructor(cordappsForAllNodes: Collection<TestCordapp>) : this(isDebug = false, cordappsForAllNodes = cordappsForAllNodes) constructor(cordappsForAllNodes: Collection<TestCordapp>) : this(isDebug = false, cordappsForAllNodes = cordappsForAllNodes)
@ -427,6 +429,7 @@ data class DriverParameters(
fun withDjvmBootstrapSource(djvmBootstrapSource: Path?): DriverParameters = copy(djvmBootstrapSource = djvmBootstrapSource) fun withDjvmBootstrapSource(djvmBootstrapSource: Path?): DriverParameters = copy(djvmBootstrapSource = djvmBootstrapSource)
fun withDjvmCordaSource(djvmCordaSource: List<Path>): DriverParameters = copy(djvmCordaSource = djvmCordaSource) fun withDjvmCordaSource(djvmCordaSource: List<Path>): DriverParameters = copy(djvmCordaSource = djvmCordaSource)
fun withEnvironmentVariables(variables : Map<String, String>): DriverParameters = copy(environmentVariables = variables) fun withEnvironmentVariables(variables : Map<String, String>): DriverParameters = copy(environmentVariables = variables)
fun withAllowHibernateToManageAppSchema(value: Boolean): DriverParameters = copy(allowHibernateToManageAppSchema = value)
fun copy( fun copy(
isDebug: Boolean, isDebug: Boolean,

Some files were not shown because too many files have changed in this diff Show More