mirror of
https://github.com/corda/corda.git
synced 2024-12-20 05:28:21 +00:00
Merge branch 'release/os/4.6' into os_4.6-feature_pass_in_client_id_when_starting_a_flow
This commit is contained in:
commit
2afedeabb4
@ -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 {
|
||||||
|
6
.ci/dev/regression/Jenkinsfile
vendored
6
.ci/dev/regression/Jenkinsfile
vendored
@ -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 {
|
||||||
|
@ -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)
|
||||||
|
184
build.gradle
184
build.gradle
@ -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'
|
|
||||||
|
@ -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()
|
||||||
|
@ -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)
|
||||||
}
|
}
|
@ -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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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<Attachment>, params: NetworkParameters, txId: SecureHash, isAttachmentTrusted: (Attachment) -> Boolean, parent: ClassLoader = ClassLoader.getSystemClassLoader(), block: (ClassLoader) -> T)</ID>
|
|
||||||
<ID>LongParameterList:BFTSmart.kt$BFTSmart.Replica$( states: List<StateRef>, txId: SecureHash, callerName: CordaX500Name, requestSignature: NotarisationRequestSignature, timeWindow: TimeWindow?, references: List<StateRef> = emptyList() )</ID>
|
<ID>LongParameterList:BFTSmart.kt$BFTSmart.Replica$( states: List<StateRef>, txId: SecureHash, callerName: CordaX500Name, requestSignature: NotarisationRequestSignature, timeWindow: TimeWindow?, references: List<StateRef> = 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<State>, outputs: List<State>, tx: LedgerTransaction, issueCommand: CommandWithParties<Commands.Issue>, currency: Currency, issuer: PartyAndReference)</ID>
|
<ID>LongParameterList:Cash.kt$Cash$(inputs: List<State>, outputs: List<State>, tx: LedgerTransaction, issueCommand: CommandWithParties<Commands.Issue>, 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<Date, Date>, 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<Date, Date>, 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<Date, Date>, 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<Date, Date>, 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<Duration, Duration> = 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<Duration, Duration> = DEFAULT_VALIDITY_WINDOW, nameConstraints: NameConstraints? = null, crlDistPoint: String? = null, crlIssuer: X500Name? = null)</ID>
|
||||||
<ID>LongParameterList:internalAccessTestHelpers.kt$( inputs: List<StateAndRef<ContractState>>, outputs: List<TransactionState<ContractState>>, commands: List<CommandWithParties<CommandData>>, attachments: List<Attachment>, id: SecureHash, notary: Party?, timeWindow: TimeWindow?, privacySalt: PrivacySalt, networkParameters: NetworkParameters, references: List<StateAndRef<ContractState>>, componentGroups: List<ComponentGroup>? = null, serializedInputs: List<SerializedStateAndRef>? = null, serializedReferences: List<SerializedStateAndRef>? = null, isAttachmentTrusted: (Attachment) -> Boolean )</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<ContractClassName, ContractAttachment></ID>
|
<ID>ThrowsCount:TransactionVerifierServiceInternal.kt$Verifier$ private fun getUniqueContractAttachmentsByContract(): Map<ContractClassName, ContractAttachment></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 -> b // c -> b and a -> b // b -> a b -> c // do not satisfy the bi-directionality (full cycle) property. // // In the first example "b" appears twice in encumbrance ("to") list and "c" exists in the encumbered ("from") list only. // Due the above, one could consume "a" and "b" in the same transaction and then, because "b" is already consumed, "c" cannot be spent. // // Similarly, the second example does not form a full cycle because "a" and "c" exist in one of the lists only. // As a result, one can consume "b" and "c" in the same transactions, which will make "a" impossible to be spent. // // On other hand the following are valid constructions: // a -> b a -> c // b -> c and c -> b // c -> a b -> a // and form a full cycle, meaning that the bi-directionality property is satisfied. private fun checkBidirectionalOutputEncumbrances(statesAndEncumbrance: List<Pair<Int, Int>>)</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 -> b // c -> b and a -> b // b -> a b -> c // do not satisfy the bi-directionality (full cycle) property. // // In the first example "b" appears twice in encumbrance ("to") list and "c" exists in the encumbered ("from") list only. // Due the above, one could consume "a" and "b" in the same transaction and then, because "b" is already consumed, "c" cannot be spent. // // Similarly, the second example does not form a full cycle because "a" and "c" exist in one of the lists only. // As a result, one can consume "b" and "c" in the same transactions, which will make "a" impossible to be spent. // // On other hand the following are valid constructions: // a -> b a -> c // b -> c and c -> b // c -> a b -> a // and form a full cycle, meaning that the bi-directionality property is satisfied. private fun checkBidirectionalOutputEncumbrances(statesAndEncumbrance: List<Pair<Int, Int>>)</ID>
|
||||||
<ID>ThrowsCount:WireTransaction.kt$WireTransaction$private fun toLedgerTransactionInternal( resolveIdentity: (PublicKey) -> Party?, resolveAttachment: (SecureHash) -> Attachment?, resolveStateRefAsSerialized: (StateRef) -> SerializedBytes<TransactionState<ContractState>>?, resolveParameters: (SecureHash?) -> NetworkParameters?, isAttachmentTrusted: (Attachment) -> Boolean ): LedgerTransaction</ID>
|
|
||||||
<ID>ThrowsCount:WireTransaction.kt$WireTransaction.Companion$ @CordaInternal fun resolveStateRefBinaryComponent(stateRef: StateRef, services: ServicesForResolution): SerializedBytes<TransactionState<ContractState>>?</ID>
|
<ID>ThrowsCount:WireTransaction.kt$WireTransaction.Companion$ @CordaInternal fun resolveStateRefBinaryComponent(stateRef: StateRef, services: ServicesForResolution): SerializedBytes<TransactionState<ContractState>>?</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>
|
||||||
|
BIN
lib/quasar.jar
BIN
lib/quasar.jar
Binary file not shown.
@ -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) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -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 })
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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>
|
@ -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
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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) }
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
@ -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())
|
||||||
|
@ -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)
|
||||||
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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"
|
||||||
|
@ -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()
|
||||||
|
@ -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),
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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!");
|
||||||
|
}
|
||||||
|
}
|
@ -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()));
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
@ -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"
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)) {
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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)
|
||||||
|
@ -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 ->
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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) })
|
||||||
|
@ -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)
|
||||||
|
@ -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) {
|
||||||
|
@ -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()
|
||||||
|
@ -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) {
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -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()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -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? {
|
||||||
|
@ -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)
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 })
|
||||||
|
}
|
||||||
}
|
}
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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>)
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
@ -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> {
|
||||||
|
@ -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.
|
||||||
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
@ -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() {
|
||||||
|
@ -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.
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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(
|
||||||
|
@ -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,
|
||||||
|
@ -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)
|
||||||
|
@ -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")
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
additionalP2PAddresses = []
|
additionalP2PAddresses = []
|
||||||
crlCheckSoftFail = true
|
crlCheckSoftFail = true
|
||||||
database = {
|
database = {
|
||||||
transactionIsolationLevel = "REPEATABLE_READ"
|
|
||||||
exportHibernateJMXStatistics = "false"
|
exportHibernateJMXStatistics = "false"
|
||||||
}
|
}
|
||||||
dataSourceProperties = {
|
dataSourceProperties = {
|
||||||
|
@ -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 {
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
@ -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())) {
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,6 @@ dataSourceProperties = {
|
|||||||
dataSource.password = ""
|
dataSource.password = ""
|
||||||
}
|
}
|
||||||
database = {
|
database = {
|
||||||
transactionIsolationLevel = "REPEATABLE_READ"
|
|
||||||
exportHibernateJMXStatistics = "false"
|
exportHibernateJMXStatistics = "false"
|
||||||
}
|
}
|
||||||
p2pAddress = "localhost:2233"
|
p2pAddress = "localhost:2233"
|
||||||
|
@ -11,7 +11,6 @@ dataSourceProperties = {
|
|||||||
dataSource.password = ""
|
dataSource.password = ""
|
||||||
}
|
}
|
||||||
database = {
|
database = {
|
||||||
transactionIsolationLevel = "REPEATABLE_READ"
|
|
||||||
exportHibernateJMXStatistics = "false"
|
exportHibernateJMXStatistics = "false"
|
||||||
}
|
}
|
||||||
p2pAddress = "localhost:2233"
|
p2pAddress = "localhost:2233"
|
||||||
|
@ -12,7 +12,6 @@ dataSourceProperties = {
|
|||||||
dataSource.password = ""
|
dataSource.password = ""
|
||||||
}
|
}
|
||||||
database = {
|
database = {
|
||||||
transactionIsolationLevel = "REPEATABLE_READ"
|
|
||||||
exportHibernateJMXStatistics = "false"
|
exportHibernateJMXStatistics = "false"
|
||||||
}
|
}
|
||||||
p2pAddress = "localhost:2233"
|
p2pAddress = "localhost:2233"
|
||||||
|
@ -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"
|
||||||
|
@ -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"
|
||||||
|
@ -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"
|
||||||
|
@ -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"
|
||||||
|
@ -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()
|
||||||
|
@ -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"
|
||||||
|
@ -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"
|
||||||
|
@ -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"
|
||||||
|
@ -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"
|
||||||
|
@ -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
Loading…
Reference in New Issue
Block a user