mirror of
https://github.com/corda/corda.git
synced 2024-12-18 20:47:57 +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 {
|
||||
/*
|
||||
* Temporarily disable Sonatype checks for regression builds
|
||||
*/
|
||||
stage('Sonatype Check') {
|
||||
when {
|
||||
expression { isReleaseTag }
|
||||
}
|
||||
steps {
|
||||
sh "./gradlew --no-daemon clean jar"
|
||||
script {
|
||||
|
6
.ci/dev/regression/Jenkinsfile
vendored
6
.ci/dev/regression/Jenkinsfile
vendored
@ -55,7 +55,13 @@ pipeline {
|
||||
}
|
||||
|
||||
stages {
|
||||
/*
|
||||
* Temporarily disable Sonatype checks for regression builds
|
||||
*/
|
||||
stage('Sonatype Check') {
|
||||
when {
|
||||
expression { isReleaseTag }
|
||||
}
|
||||
steps {
|
||||
sh "./gradlew --no-daemon clean jar"
|
||||
script {
|
||||
|
@ -13,6 +13,7 @@ see changes to this list.
|
||||
* agoldvarg
|
||||
* Ajitha Thayaharan (BCS Technology International)
|
||||
* Alberto Arri (R3)
|
||||
* Alex Karnezis
|
||||
* amiracam
|
||||
* Amol Pednekar
|
||||
* Andras Slemmer (R3)
|
||||
|
186
build.gradle
186
build.gradle
@ -1,5 +1,4 @@
|
||||
import com.r3.testing.DistributeTestsBy
|
||||
import com.r3.testing.ParallelTestGroup
|
||||
import com.r3.testing.PodLogLevel
|
||||
|
||||
import static org.gradle.api.JavaVersion.VERSION_11
|
||||
@ -172,16 +171,30 @@ buildscript {
|
||||
}
|
||||
}
|
||||
} 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()
|
||||
jcenter()
|
||||
maven {
|
||||
url 'https://kotlin.bintray.com/kotlinx'
|
||||
}
|
||||
maven {
|
||||
url "${artifactory_contextUrl}/corda-dependencies-dev"
|
||||
}
|
||||
maven {
|
||||
url "${artifactory_contextUrl}/corda-releases"
|
||||
content {
|
||||
includeGroup 'org.jetbrains.kotlin'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -204,11 +217,13 @@ buildscript {
|
||||
// Capsule gradle plugin forked and maintained locally to support Gradle 5.x
|
||||
// See https://github.com/corda/gradle-capsule-plugin
|
||||
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.dependx", name: "gradle-dependx", version: "0.1.13", changing: true
|
||||
classpath "com.bmuschko:gradle-docker-plugin:5.0.0"
|
||||
classpath group: "com.r3.testing", name: "gradle-distributed-testing-plugin", version: "1.3-SNAPSHOT", changing: true
|
||||
classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.8"
|
||||
}
|
||||
|
||||
configurations.all {
|
||||
resolutionStrategy.cacheChangingModulesFor 0, 'seconds'
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
@ -222,8 +237,7 @@ apply plugin: 'project-report'
|
||||
apply plugin: 'com.github.ben-manes.versions'
|
||||
apply plugin: 'net.corda.plugins.publish-utils'
|
||||
apply plugin: 'com.jfrog.artifactory'
|
||||
apply plugin: "com.bmuschko.docker-remote-api"
|
||||
apply plugin: "com.r3.dependx.dependxies"
|
||||
apply plugin: 'com.r3.testing.distributed-testing'
|
||||
|
||||
|
||||
// If the command line project option -PversionFromGit is added to the gradle invocation, we'll resolve
|
||||
@ -390,11 +404,32 @@ allprojects {
|
||||
}
|
||||
}
|
||||
} 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()
|
||||
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) {
|
||||
baseName = "api-corda"
|
||||
}
|
||||
@ -705,83 +735,45 @@ buildScan {
|
||||
termsOfServiceAgree = 'yes'
|
||||
}
|
||||
|
||||
ext.generalPurpose = [
|
||||
numberOfShards: 15,
|
||||
streamOutput: false,
|
||||
coresPerFork: 2,
|
||||
memoryInGbPerFork: 12,
|
||||
nodeTaints: "small"
|
||||
]
|
||||
distributedTesting {
|
||||
profilesURL = 'https://raw.githubusercontent.com/corda/infrastructure-profiles/master'
|
||||
|
||||
ext.largeScaleSet = [
|
||||
numberOfShards: 15,
|
||||
streamOutput: false,
|
||||
coresPerFork: 6,
|
||||
memoryInGbPerFork: 10,
|
||||
nodeTaints: "big"
|
||||
]
|
||||
parallelTestGroups {
|
||||
allParallelIntegrationTest {
|
||||
testGroups 'integrationTest'
|
||||
profile 'generalPurpose.yml'
|
||||
podLogLevel PodLogLevel.INFO
|
||||
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) {
|
||||
dependsOn dependxiesModule
|
||||
podLogLevel PodLogLevel.INFO
|
||||
testGroups "integrationTest"
|
||||
numberOfShards generalPurpose.numberOfShards
|
||||
streamOutput generalPurpose.streamOutput
|
||||
coresPerFork generalPurpose.coresPerFork
|
||||
memoryInGbPerFork generalPurpose.memoryInGbPerFork
|
||||
nodeTaints generalPurpose.nodeTaints
|
||||
distribute DistributeTestsBy.METHOD
|
||||
ignoredTests = [
|
||||
':core-deterministic:testing:data:test'
|
||||
]
|
||||
}
|
||||
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.RPCException
|
||||
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.Trace
|
||||
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.CallSiteMap
|
||||
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 org.apache.activemq.artemis.api.core.ActiveMQException
|
||||
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 rx.Notification
|
||||
import rx.Observable
|
||||
import rx.exceptions.OnErrorNotImplementedException
|
||||
import rx.subjects.UnicastSubject
|
||||
import java.lang.reflect.InvocationHandler
|
||||
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
|
||||
@ -452,14 +466,9 @@ internal class RPCClientProxyHandler(
|
||||
}
|
||||
|
||||
reaperScheduledFuture?.cancel(false)
|
||||
val observablesMap = observableContext.observableMap.asMap()
|
||||
observablesMap.keys.forEach { key ->
|
||||
observableContext.observableMap.asMap().forEach { (key, observable) ->
|
||||
observationExecutorPool.run(key) {
|
||||
try {
|
||||
observablesMap[key]?.onError(ConnectionFailureException())
|
||||
} catch (e: Exception) {
|
||||
log.error("Unexpected exception when RPC connection failure handling", e)
|
||||
}
|
||||
observable?.also(Companion::closeObservable)
|
||||
}
|
||||
}
|
||||
observableContext.observableMap.invalidateAll()
|
||||
|
@ -9,26 +9,32 @@ import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class ExceptionsErrorCodeFunctionsTest {
|
||||
|
||||
@Test(timeout=3_000)
|
||||
fun `error code for message prints out message and full stack trace`() {
|
||||
val originalMessage = SimpleMessage("This is a test message")
|
||||
var previous: Exception? = null
|
||||
val throwables = (0..10).map {
|
||||
val current = TestThrowable(it, previous)
|
||||
previous = current
|
||||
current
|
||||
private companion object {
|
||||
private const val EXCEPTION_MESSAGE = "This is exception "
|
||||
private const val TEST_MESSAGE = "This is a test message"
|
||||
private fun makeChain(previous: Exception?, ttl: Int): Exception {
|
||||
val current = TestThrowable(ttl, previous)
|
||||
return if (ttl == 0) {
|
||||
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)
|
||||
assertThat(message.formattedMessage, contains("This is a test message".toRegex()))
|
||||
assertThat(message.formattedMessage, contains(TEST_MESSAGE.toRegex()))
|
||||
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.parameters, originalMessage.parameters)
|
||||
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
|
||||
guavaVersion=28.0-jre
|
||||
# Quasar version to use with Java 8:
|
||||
quasarVersion=0.7.12_r3
|
||||
quasarClassifier=jdk8
|
||||
quasarVersion=0.7.13_r3
|
||||
# Quasar version to use with Java 11:
|
||||
quasarVersion11=0.8.0_r3
|
||||
jdkClassifier11=jdk11
|
||||
|
@ -25,10 +25,7 @@ tasks.named('jar', Jar) {
|
||||
enabled = false
|
||||
}
|
||||
|
||||
test {
|
||||
ext {
|
||||
ignoreForDistribution = true
|
||||
}
|
||||
def test = tasks.named('test', Test) {
|
||||
filter {
|
||||
// Running this class is the whole point, so include it explicitly.
|
||||
includeTestsMatching "net.corda.deterministic.data.GenerateData"
|
||||
@ -37,8 +34,9 @@ test {
|
||||
// note: required by Gradle Build Cache.
|
||||
outputs.upToDateWhen { false }
|
||||
}
|
||||
assemble.finalizedBy test
|
||||
|
||||
def testDataJar = file("$buildDir/test-data.jar")
|
||||
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 */
|
||||
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)
|
||||
|
||||
// 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()
|
||||
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, 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: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: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>
|
||||
@ -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, 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: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:AMQPSerializationScheme.kt$AbstractAMQPSerializationScheme$128</ID>
|
||||
<ID>MagicNumber:AMQPServer.kt$AMQPServer$100</ID>
|
||||
@ -1284,7 +1282,6 @@
|
||||
<ID>SpreadOperator:FlowFrameworkTripartyTests.kt$FlowFrameworkTripartyTests$(*expected)</ID>
|
||||
<ID>SpreadOperator:FlowLogicRefFactoryImpl.kt$FlowLogicRefFactoryImpl$(flowClass, *args)</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$(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>
|
||||
@ -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: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: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>TooGenericExceptionCaught:AMQPChannelHandler.kt$AMQPChannelHandler$ex: Exception</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$exception: Throwable</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:EventProcessor.kt$EventProcessor$ex: 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:RPCClientProxyHandler.kt$RPCClientProxyHandler : InvocationHandler</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:ServiceHub.kt$ServiceHub : ServicesForResolution</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.newSecureRandom
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.internal.JavaVersion
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.serialization.deserialize
|
||||
@ -118,7 +117,7 @@ class X509UtilitiesTest {
|
||||
@Test(timeout=300_000)
|
||||
fun `create valid self-signed CA certificate`() {
|
||||
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) {
|
||||
@ -153,7 +152,7 @@ class X509UtilitiesTest {
|
||||
|
||||
@Test(timeout=300_000)
|
||||
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) }
|
||||
}
|
||||
|
||||
|
@ -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.PersistentState
|
||||
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
||||
import net.corda.nodeapi.internal.persistence.MissingMigrationException
|
||||
import net.corda.nodeapi.internal.persistence.SchemaMigration
|
||||
import net.corda.node.internal.DataSourceFactory
|
||||
import net.corda.node.services.persistence.DBCheckpointStorage
|
||||
import net.corda.node.services.schema.NodeSchemaService
|
||||
import net.corda.nodeapi.internal.persistence.MissingMigrationException
|
||||
import net.corda.nodeapi.internal.persistence.SchemaMigration
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.TestIdentity
|
||||
import net.corda.testing.node.MockServices
|
||||
@ -40,25 +39,21 @@ class MissingSchemaMigrationTest {
|
||||
dataSource = DataSourceFactory.createDataSource(hikariProperties)
|
||||
}
|
||||
|
||||
private fun createSchemaMigration(schemasToMigrate: Set<MappedSchema>, forceThrowOnMissingMigration: Boolean): SchemaMigration {
|
||||
val databaseConfig = DatabaseConfig()
|
||||
return SchemaMigration(schemasToMigrate, dataSource, databaseConfig, null, null,
|
||||
TestIdentity(ALICE_NAME, 70).name, forceThrowOnMissingMigration)
|
||||
}
|
||||
private fun schemaMigration() = SchemaMigration(dataSource, null, null,
|
||||
TestIdentity(ALICE_NAME, 70).name)
|
||||
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `test that an error is thrown when forceThrowOnMissingMigration is set and a mapped schema is missing a migration`() {
|
||||
assertThatThrownBy {
|
||||
createSchemaMigration(setOf(GoodSchema), true)
|
||||
.nodeStartup(dataSource.connection.use { DBCheckpointStorage.getCheckpointCount(it) != 0L })
|
||||
schemaMigration().runMigration(dataSource.connection.use { DBCheckpointStorage.getCheckpointCount(it) != 0L }, setOf(GoodSchema), true)
|
||||
}.isInstanceOf(MissingMigrationException::class.java)
|
||||
}
|
||||
|
||||
@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`() {
|
||||
assertDoesNotThrow {
|
||||
createSchemaMigration(setOf(GoodSchema), false)
|
||||
.nodeStartup(dataSource.connection.use { DBCheckpointStorage.getCheckpointCount(it) != 0L })
|
||||
schemaMigration().runMigration(dataSource.connection.use { DBCheckpointStorage.getCheckpointCount(it) != 0L }, setOf(GoodSchema), false)
|
||||
}
|
||||
}
|
||||
|
||||
@ -66,8 +61,7 @@ class MissingSchemaMigrationTest {
|
||||
fun `test that there are no missing migrations for the node`() {
|
||||
assertDoesNotThrow("This test failure indicates " +
|
||||
"a new table has been added to the node without the appropriate migration scripts being present") {
|
||||
createSchemaMigration(NodeSchemaService().internalSchemas(), false)
|
||||
.nodeStartup(dataSource.connection.use { DBCheckpointStorage.getCheckpointCount(it) != 0L })
|
||||
schemaMigration().runMigration(dataSource.connection.use { DBCheckpointStorage.getCheckpointCount(it) != 0L }, NodeSchemaService().internalSchemas, true)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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"
|
||||
)
|
||||
|
||||
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 val jarsThatArentCordapps = setOf("corda.jar", "runnodes.jar")
|
||||
@ -92,7 +101,9 @@ constructor(private val initSerEnv: Boolean,
|
||||
}
|
||||
val executor = Executors.newFixedThreadPool(numParallelProcesses)
|
||||
return try {
|
||||
nodeDirs.map { executor.fork { generateNodeInfo(it) } }.transpose().getOrThrow()
|
||||
nodeDirs.map { executor.fork {
|
||||
createDbSchemas(it)
|
||||
generateNodeInfo(it) } }.transpose().getOrThrow()
|
||||
} finally {
|
||||
warningTimer.cancel()
|
||||
executor.shutdownNow()
|
||||
@ -100,23 +111,31 @@ constructor(private val initSerEnv: Boolean,
|
||||
}
|
||||
|
||||
private fun generateNodeInfo(nodeDir: Path): Path {
|
||||
runNodeJob(nodeInfoGenCmd, nodeDir, "node-info-gen.log")
|
||||
return nodeDir.list { paths ->
|
||||
paths.filter { it.fileName.toString().startsWith(NODE_INFO_FILE_NAME_PREFIX) }.findFirst().get()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createDbSchemas(nodeDir: Path) {
|
||||
runNodeJob(createSchemasCmd, nodeDir, "node-run-migration.log")
|
||||
}
|
||||
|
||||
private fun runNodeJob(command: List<String>, nodeDir: Path, logfileName: String) {
|
||||
val logsDir = (nodeDir / LOGS_DIR_NAME).createDirectories()
|
||||
val nodeInfoGenFile = (logsDir / "node-info-gen.log").toFile()
|
||||
val process = ProcessBuilder(nodeInfoGenCmd)
|
||||
val nodeRedirectFile = (logsDir / logfileName).toFile()
|
||||
val process = ProcessBuilder(command)
|
||||
.directory(nodeDir.toFile())
|
||||
.redirectErrorStream(true)
|
||||
.redirectOutput(nodeInfoGenFile)
|
||||
.redirectOutput(nodeRedirectFile)
|
||||
.apply { environment()["CAPSULE_CACHE_DIR"] = "../.cache" }
|
||||
.start()
|
||||
try {
|
||||
if (!process.waitFor(3, TimeUnit.MINUTES)) {
|
||||
process.destroyForcibly()
|
||||
printNodeInfoGenLogToConsole(nodeInfoGenFile)
|
||||
}
|
||||
printNodeInfoGenLogToConsole(nodeInfoGenFile) { process.exitValue() == 0 }
|
||||
return nodeDir.list { paths ->
|
||||
paths.filter { it.fileName.toString().startsWith(NODE_INFO_FILE_NAME_PREFIX) }.findFirst().get()
|
||||
printNodeOutputToConsoleAndThrow(nodeRedirectFile)
|
||||
}
|
||||
if (process.exitValue() != 0) printNodeOutputToConsoleAndThrow(nodeRedirectFile)
|
||||
} catch (e: InterruptedException) {
|
||||
// Don't leave this process dangling if the thread is interrupted.
|
||||
process.destroyForcibly()
|
||||
@ -124,18 +143,16 @@ constructor(private val initSerEnv: Boolean,
|
||||
}
|
||||
}
|
||||
|
||||
private fun printNodeInfoGenLogToConsole(nodeInfoGenFile: File, check: (() -> Boolean) = { true }) {
|
||||
if (!check.invoke()) {
|
||||
val nodeDir = nodeInfoGenFile.parent
|
||||
val nodeIdentifier = try {
|
||||
ConfigFactory.parseFile((nodeDir / "node.conf").toFile()).getString("myLegalName")
|
||||
} catch (e: ConfigException) {
|
||||
nodeDir
|
||||
}
|
||||
System.err.println("#### Error while generating node info file $nodeIdentifier ####")
|
||||
nodeInfoGenFile.inputStream().copyTo(System.err)
|
||||
throw IllegalStateException("Error while generating node info file. Please check the logs in $nodeDir.")
|
||||
private fun printNodeOutputToConsoleAndThrow(stdoutFile: File) {
|
||||
val nodeDir = stdoutFile.parent
|
||||
val nodeIdentifier = try {
|
||||
ConfigFactory.parseFile((nodeDir / "node.conf").toFile()).getString("myLegalName")
|
||||
} catch (e: ConfigException) {
|
||||
nodeDir
|
||||
}
|
||||
System.err.println("#### Error while generating node info file $nodeIdentifier ####")
|
||||
stdoutFile.inputStream().copyTo(System.err)
|
||||
throw IllegalStateException("Error while generating node info file. Please check the logs in $nodeDir.")
|
||||
}
|
||||
|
||||
const val DEFAULT_MAX_MESSAGE_SIZE: Int = 10485760
|
||||
|
@ -31,24 +31,12 @@ import javax.sql.DataSource
|
||||
*/
|
||||
const val NODE_DATABASE_PREFIX = "node_"
|
||||
|
||||
enum class SchemaInitializationType{
|
||||
NONE,
|
||||
VALIDATE,
|
||||
UPDATE
|
||||
}
|
||||
|
||||
// This class forms part of the node config and so any changes to it must be handled with care
|
||||
data class DatabaseConfig(
|
||||
val initialiseSchema: Boolean = Defaults.initialiseSchema,
|
||||
val initialiseAppSchema: SchemaInitializationType = Defaults.initialiseAppSchema,
|
||||
val transactionIsolationLevel: TransactionIsolationLevel = Defaults.transactionIsolationLevel,
|
||||
val exportHibernateJMXStatistics: Boolean = Defaults.exportHibernateJMXStatistics,
|
||||
val mappedSchemaCacheSize: Long = Defaults.mappedSchemaCacheSize
|
||||
) {
|
||||
object Defaults {
|
||||
val initialiseSchema = true
|
||||
val initialiseAppSchema = SchemaInitializationType.UPDATE
|
||||
val transactionIsolationLevel = TransactionIsolationLevel.REPEATABLE_READ
|
||||
val exportHibernateJMXStatistics = false
|
||||
val mappedSchemaCacheSize = 100L
|
||||
}
|
||||
@ -67,6 +55,10 @@ enum class TransactionIsolationLevel {
|
||||
*/
|
||||
val jdbcString = "TRANSACTION_$name"
|
||||
val jdbcValue: Int = java.sql.Connection::class.java.getField(jdbcString).get(null) as Int
|
||||
|
||||
companion object{
|
||||
val default = READ_COMMITTED
|
||||
}
|
||||
}
|
||||
|
||||
internal val _prohibitDatabaseAccess = ThreadLocal.withInitial { false }
|
||||
@ -96,27 +88,28 @@ fun <T> withoutDatabaseAccess(block: () -> T): T {
|
||||
val contextDatabaseOrNull: CordaPersistence? get() = _contextDatabase.get()
|
||||
|
||||
class CordaPersistence(
|
||||
databaseConfig: DatabaseConfig,
|
||||
exportHibernateJMXStatistics: Boolean,
|
||||
schemas: Set<MappedSchema>,
|
||||
val jdbcUrl: String,
|
||||
cacheFactory: NamedCacheFactory,
|
||||
attributeConverters: Collection<AttributeConverter<*, *>> = emptySet(),
|
||||
customClassLoader: ClassLoader? = null,
|
||||
val closeConnection: Boolean = true,
|
||||
val errorHandler: DatabaseTransaction.(e: Exception) -> Unit = {}
|
||||
val errorHandler: DatabaseTransaction.(e: Exception) -> Unit = {},
|
||||
allowHibernateToManageAppSchema: Boolean = false
|
||||
) : Closeable {
|
||||
companion object {
|
||||
private val log = contextLogger()
|
||||
}
|
||||
|
||||
private val defaultIsolationLevel = databaseConfig.transactionIsolationLevel
|
||||
private val defaultIsolationLevel = TransactionIsolationLevel.default
|
||||
val hibernateConfig: HibernateConfiguration by lazy {
|
||||
transaction {
|
||||
try {
|
||||
HibernateConfiguration(schemas, databaseConfig, attributeConverters, jdbcUrl, cacheFactory, customClassLoader)
|
||||
HibernateConfiguration(schemas, exportHibernateJMXStatistics, attributeConverters, jdbcUrl, cacheFactory, customClassLoader, allowHibernateToManageAppSchema)
|
||||
} catch (e: Exception) {
|
||||
when (e) {
|
||||
is SchemaManagementException -> throw HibernateSchemaChangeException("Incompatible schema change detected. Please run the node with database.initialiseSchema=true. Reason: ${e.message}", e)
|
||||
is SchemaManagementException -> throw HibernateSchemaChangeException("Incompatible schema change detected. Please run schema migration scripts (node with sub-command run-migration-scripts). Reason: ${e.message}", e)
|
||||
else -> throw HibernateConfigException("Could not create Hibernate configuration: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
@ -19,11 +19,12 @@ import javax.persistence.AttributeConverter
|
||||
|
||||
class HibernateConfiguration(
|
||||
schemas: Set<MappedSchema>,
|
||||
private val databaseConfig: DatabaseConfig,
|
||||
private val exportHibernateJMXStatistics: Boolean,
|
||||
private val attributeConverters: Collection<AttributeConverter<*, *>>,
|
||||
jdbcUrl: String,
|
||||
cacheFactory: NamedCacheFactory,
|
||||
val customClassLoader: ClassLoader? = null
|
||||
val customClassLoader: ClassLoader? = null,
|
||||
val allowHibernateToManageAppSchema: Boolean = false
|
||||
) {
|
||||
companion object {
|
||||
private val logger = contextLogger()
|
||||
@ -64,10 +65,10 @@ class HibernateConfiguration(
|
||||
fun sessionFactoryForSchemas(key: Set<MappedSchema>): SessionFactory = sessionFactories.get(key, ::makeSessionFactoryForSchemas)!!
|
||||
|
||||
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
|
||||
if (databaseConfig.exportHibernateJMXStatistics)
|
||||
if (exportHibernateJMXStatistics)
|
||||
initStatistics(sessionFactory)
|
||||
|
||||
return sessionFactory
|
||||
@ -75,7 +76,7 @@ class HibernateConfiguration(
|
||||
|
||||
// NOTE: workaround suggested to overcome deprecation of StatisticsService (since Hibernate v4.0)
|
||||
// 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 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.LabelExpression
|
||||
import liquibase.Liquibase
|
||||
import liquibase.database.Database
|
||||
import liquibase.database.DatabaseFactory
|
||||
import liquibase.database.jvm.JdbcConnection
|
||||
import liquibase.exception.LiquibaseException
|
||||
import liquibase.resource.ClassLoaderResourceAccessor
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.nodeapi.internal.MigrationHelpers.getMigrationResource
|
||||
import net.corda.core.schemas.MappedSchema
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.nodeapi.internal.MigrationHelpers.getMigrationResource
|
||||
import net.corda.nodeapi.internal.cordapp.CordappLoader
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.InputStream
|
||||
import java.nio.file.Path
|
||||
import java.sql.Statement
|
||||
import javax.sql.DataSource
|
||||
import java.sql.Connection
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import javax.sql.DataSource
|
||||
import kotlin.concurrent.withLock
|
||||
|
||||
// Migrate the database to the current version, using liquibase.
|
||||
class SchemaMigration(
|
||||
val schemas: Set<MappedSchema>,
|
||||
open class SchemaMigration(
|
||||
val dataSource: DataSource,
|
||||
private val databaseConfig: DatabaseConfig,
|
||||
cordappLoader: CordappLoader? = null,
|
||||
private val currentDirectory: Path?,
|
||||
// This parameter is used by the vault state migration to establish what the node's legal identity is when setting up
|
||||
// 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.
|
||||
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
|
||||
// missing, so no need to throw unless you're specifically testing whether all the migrations are present.
|
||||
private val forceThrowOnMissingMigration: Boolean = false) {
|
||||
protected val databaseFactory: LiquibaseDatabaseFactory = LiquibaseDatabaseFactoryImpl()) {
|
||||
|
||||
companion object {
|
||||
private val logger = contextLogger()
|
||||
const val NODE_BASE_DIR_KEY = "liquibase.nodeDaseDir"
|
||||
const val NODE_X500_NAME = "liquibase.nodeName"
|
||||
val loader = ThreadLocal<CordappLoader>()
|
||||
private val mutex = ReentrantLock()
|
||||
@JvmStatic
|
||||
protected val mutex = ReentrantLock()
|
||||
}
|
||||
|
||||
init {
|
||||
@ -50,36 +46,86 @@ class SchemaMigration(
|
||||
|
||||
private val classLoader = cordappLoader?.appClassLoader ?: Thread.currentThread().contextClassLoader
|
||||
|
||||
/**
|
||||
* Main entry point to the schema migration.
|
||||
* Called during node startup.
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
fun nodeStartup(existingCheckpoints: Boolean) {
|
||||
when {
|
||||
databaseConfig.initialiseSchema -> {
|
||||
migrateOlderDatabaseToUseLiquibase(existingCheckpoints)
|
||||
runMigration(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.
|
||||
* @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.
|
||||
*/
|
||||
fun checkState(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 (_, changeToRunCount, _) = prepareRunner(connection, resourcesAndSourceInfo)
|
||||
if (changeToRunCount > 0)
|
||||
throw OutstandingDatabaseChangesException(changeToRunCount)
|
||||
}
|
||||
else -> checkState()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Will run the Liquibase migration on the actual database.
|
||||
* 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
|
||||
*/
|
||||
private fun runMigration(existingCheckpoints: Boolean) = doRunMigration(run = true, check = false, existingCheckpoints = existingCheckpoints)
|
||||
fun synchroniseSchemas(schemas: Set<MappedSchema>, forceThrowOnMissingMigration: Boolean) {
|
||||
val resourcesAndSourceInfo = prepareResources(schemas, forceThrowOnMissingMigration)
|
||||
|
||||
/**
|
||||
* Ensures that the database is up to date with the latest migration changes.
|
||||
*/
|
||||
private fun checkState() = doRunMigration(run = false, check = true)
|
||||
// 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 resourse accessor that aggregates the changelogs included in the schemas into one dynamic stream. */
|
||||
private class CustomResourceAccessor(val dynamicInclude: String, val changelogList: List<String?>, classLoader: ClassLoader) : ClassLoaderResourceAccessor(classLoader) {
|
||||
/** 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> {
|
||||
if (path == dynamicInclude) {
|
||||
// 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.
|
||||
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) {
|
||||
throw MissingMigrationException(mappedSchema)
|
||||
} else {
|
||||
@ -99,143 +145,38 @@ class SchemaMigration(
|
||||
null
|
||||
}
|
||||
|
||||
private fun doRunMigration(
|
||||
run: Boolean,
|
||||
check: Boolean,
|
||||
existingCheckpoints: Boolean? = null
|
||||
) {
|
||||
// Virtual file name of the changelog that includes all schemas.
|
||||
val dynamicInclude = "master.changelog.json"
|
||||
|
||||
// Virtual file name of the changelog that includes all schemas.
|
||||
val dynamicInclude = "master.changelog.json"
|
||||
|
||||
dataSource.connection.use { connection ->
|
||||
|
||||
// Collect all changelog files referenced in the included schemas.
|
||||
val changelogList = schemas.mapNotNull { mappedSchema ->
|
||||
val resource = getMigrationResource(mappedSchema, classLoader)
|
||||
when {
|
||||
resource != null -> resource
|
||||
// Corda OS FinanceApp in v3 has no Liquibase script, so no error is raised
|
||||
(mappedSchema::class.qualifiedName == "net.corda.finance.schemas.CashSchemaV1" || mappedSchema::class.qualifiedName == "net.corda.finance.schemas.CommercialPaperSchemaV1") && mappedSchema.migrationResource == null -> null
|
||||
else -> logOrThrowMigrationError(mappedSchema)
|
||||
}
|
||||
}
|
||||
|
||||
val path = currentDirectory?.toString()
|
||||
if (path != null) {
|
||||
System.setProperty(NODE_BASE_DIR_KEY, path) // base dir for any custom change set which may need to load a file (currently AttachmentVersionNumberMigration)
|
||||
}
|
||||
if (ourName != null) {
|
||||
System.setProperty(NODE_X500_NAME, ourName.toString())
|
||||
}
|
||||
val customResourceAccessor = CustomResourceAccessor(dynamicInclude, changelogList, classLoader)
|
||||
checkResourcesInClassPath(changelogList)
|
||||
|
||||
// current version of Liquibase appears to be non-threadsafe
|
||||
// this is apparent when multiple in-process nodes are all running migrations simultaneously
|
||||
mutex.withLock {
|
||||
val liquibase = Liquibase(dynamicInclude, customResourceAccessor, getLiquibaseDatabase(JdbcConnection(connection)))
|
||||
|
||||
val unRunChanges = liquibase.listUnrunChangeSets(Contexts(), LabelExpression())
|
||||
|
||||
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.")
|
||||
}
|
||||
protected fun prepareResources(schemas: Set<MappedSchema>, forceThrowOnMissingMigration: Boolean): List<Pair<CustomResourceAccessor, String>> {
|
||||
// Collect all changelog files referenced in the included schemas.
|
||||
val changelogList = schemas.mapNotNull { mappedSchema ->
|
||||
val resource = getMigrationResource(mappedSchema, classLoader)
|
||||
when {
|
||||
resource != null -> resource
|
||||
else -> logOrThrowMigrationError(mappedSchema, forceThrowOnMissingMigration)
|
||||
}
|
||||
}
|
||||
|
||||
val path = currentDirectory?.toString()
|
||||
if (path != null) {
|
||||
System.setProperty(NODE_BASE_DIR_KEY, path) // base dir for any custom change set which may need to load a file (currently AttachmentVersionNumberMigration)
|
||||
}
|
||||
if (ourName != null) {
|
||||
System.setProperty(NODE_X500_NAME, ourName.toString())
|
||||
}
|
||||
val customResourceAccessor = CustomResourceAccessor(dynamicInclude, changelogList, classLoader)
|
||||
checkResourcesInClassPath(changelogList)
|
||||
return listOf(Pair(customResourceAccessor, ""))
|
||||
}
|
||||
|
||||
private fun getLiquibaseDatabase(conn: JdbcConnection): Database {
|
||||
return DatabaseFactory.getInstance().findCorrectDatabaseImplementation(conn)
|
||||
}
|
||||
protected fun prepareRunner(connection: Connection,
|
||||
resourcesAndSourceInfo: List<Pair<CustomResourceAccessor, String>>): Triple<Liquibase, Int, Boolean> {
|
||||
require(resourcesAndSourceInfo.size == 1)
|
||||
val liquibase = Liquibase(dynamicInclude, resourcesAndSourceInfo.single().first, databaseFactory.getLiquibaseDatabase(JdbcConnection(connection)))
|
||||
|
||||
/** 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
|
||||
val unRunChanges = liquibase.listUnrunChangeSets(Contexts(), LabelExpression())
|
||||
return Triple(liquibase, unRunChanges.size, !unRunChanges.isEmpty())
|
||||
}
|
||||
|
||||
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!!
|
||||
}
|
||||
|
||||
@ -269,6 +210,6 @@ class CheckpointsException : DatabaseMigrationException("Attempting to update th
|
||||
|
||||
class DatabaseIncompatibleException(@Suppress("MemberVisibilityCanBePrivate") private val reason: String) : DatabaseMigrationException(errorMessageFor(reason)) {
|
||||
internal companion object {
|
||||
fun errorMessageFor(reason: String): String = "Incompatible database schema version detected, please run the node with configuration option database.initialiseSchema=true. Reason: $reason"
|
||||
fun errorMessageFor(reason: String): String = "Incompatible database schema version detected, please run schema migration scripts (node with sub-command run-migration-scripts). Reason: $reason"
|
||||
}
|
||||
}
|
@ -3,9 +3,8 @@ package net.corda.nodeapi.internal.persistence.factory
|
||||
import net.corda.core.schemas.MappedSchema
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.toHexString
|
||||
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
||||
import net.corda.nodeapi.internal.persistence.HibernateConfiguration
|
||||
import net.corda.nodeapi.internal.persistence.SchemaInitializationType
|
||||
import net.corda.nodeapi.internal.persistence.TransactionIsolationLevel
|
||||
import org.hibernate.SessionFactory
|
||||
import org.hibernate.boot.Metadata
|
||||
import org.hibernate.boot.MetadataBuilder
|
||||
@ -26,22 +25,19 @@ abstract class BaseSessionFactoryFactory : CordaSessionFactoryFactory {
|
||||
private val logger = contextLogger()
|
||||
}
|
||||
|
||||
open fun buildHibernateConfig(databaseConfig: DatabaseConfig, metadataSources: MetadataSources): Configuration {
|
||||
open fun buildHibernateConfig(metadataSources: MetadataSources, allowHibernateToManageAppSchema: Boolean): Configuration {
|
||||
val hbm2dll: String =
|
||||
if (databaseConfig.initialiseSchema && databaseConfig.initialiseAppSchema == SchemaInitializationType.UPDATE) {
|
||||
if (allowHibernateToManageAppSchema) {
|
||||
"update"
|
||||
} else if ((!databaseConfig.initialiseSchema && databaseConfig.initialiseAppSchema == SchemaInitializationType.UPDATE)
|
||||
|| databaseConfig.initialiseAppSchema == SchemaInitializationType.VALIDATE) {
|
||||
} else {
|
||||
"validate"
|
||||
} else {
|
||||
"none"
|
||||
}
|
||||
// We set a connection provider as the auto schema generation requires it. The auto schema generation will not
|
||||
// necessarily remain and would likely be replaced by something like Liquibase. For now it is very convenient though.
|
||||
return Configuration(metadataSources).setProperty("hibernate.connection.provider_class", HibernateConfiguration.NodeDatabaseConnectionProvider::class.java.name)
|
||||
.setProperty("hibernate.format_sql", "true")
|
||||
.setProperty("javax.persistence.validation.mode", "none")
|
||||
.setProperty("hibernate.connection.isolation", databaseConfig.transactionIsolationLevel.jdbcValue.toString())
|
||||
.setProperty("hibernate.connection.isolation", TransactionIsolationLevel.default.jdbcValue.toString())
|
||||
.setProperty("hibernate.hbm2ddl.auto", hbm2dll)
|
||||
.setProperty("hibernate.jdbc.time_zone", "UTC")
|
||||
}
|
||||
@ -85,15 +81,15 @@ abstract class BaseSessionFactoryFactory : CordaSessionFactoryFactory {
|
||||
}
|
||||
|
||||
final override fun makeSessionFactoryForSchemas(
|
||||
databaseConfig: DatabaseConfig,
|
||||
schemas: Set<MappedSchema>,
|
||||
customClassLoader: ClassLoader?,
|
||||
attributeConverters: Collection<AttributeConverter<*, *>>): SessionFactory {
|
||||
attributeConverters: Collection<AttributeConverter<*, *>>,
|
||||
allowHibernateToMananageAppSchema: Boolean): SessionFactory {
|
||||
logger.info("Creating session factory for schemas: $schemas")
|
||||
val serviceRegistry = BootstrapServiceRegistryBuilder().build()
|
||||
val metadataSources = MetadataSources(serviceRegistry)
|
||||
|
||||
val config = buildHibernateConfig(databaseConfig, metadataSources)
|
||||
val config = buildHibernateConfig(metadataSources, allowHibernateToMananageAppSchema)
|
||||
schemas.forEach { schema ->
|
||||
schema.mappedTypes.forEach { config.addAnnotatedClass(it) }
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
package net.corda.nodeapi.internal.persistence.factory
|
||||
|
||||
import net.corda.core.schemas.MappedSchema
|
||||
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
||||
import org.hibernate.SessionFactory
|
||||
import org.hibernate.boot.Metadata
|
||||
import org.hibernate.boot.MetadataBuilder
|
||||
@ -11,10 +10,10 @@ interface CordaSessionFactoryFactory {
|
||||
val databaseType: String
|
||||
fun canHandleDatabase(jdbcUrl: String): Boolean
|
||||
fun makeSessionFactoryForSchemas(
|
||||
databaseConfig: DatabaseConfig,
|
||||
schemas: Set<MappedSchema>,
|
||||
customClassLoader: ClassLoader?,
|
||||
attributeConverters: Collection<AttributeConverter<*, *>>): SessionFactory
|
||||
attributeConverters: Collection<AttributeConverter<*, *>>,
|
||||
allowHibernateToMananageAppSchema: Boolean): SessionFactory
|
||||
fun getExtraConfiguration(key: String): Any?
|
||||
fun buildHibernateMetadata(metadataBuilder: MetadataBuilder, attributeConverters: Collection<AttributeConverter<*, *>>): Metadata
|
||||
}
|
@ -200,10 +200,7 @@ internal fun createClientSslHelper(target: NetworkHostAndPort,
|
||||
expectedRemoteLegalNames: Set<CordaX500Name>,
|
||||
keyManagerFactory: KeyManagerFactory,
|
||||
trustManagerFactory: TrustManagerFactory): SslHandler {
|
||||
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())
|
||||
val sslContext = createAndInitSslContext(keyManagerFactory, trustManagerFactory)
|
||||
val sslEngine = sslContext.createSSLEngine(target.host, target.port)
|
||||
sslEngine.useClientMode = true
|
||||
sslEngine.enabledProtocols = ArtemisTcpTransport.TLS_VERSIONS.toTypedArray()
|
||||
@ -239,10 +236,7 @@ internal fun createClientOpenSslHandler(target: NetworkHostAndPort,
|
||||
internal fun createServerSslHandler(keyStore: CertificateStore,
|
||||
keyManagerFactory: KeyManagerFactory,
|
||||
trustManagerFactory: TrustManagerFactory): SslHandler {
|
||||
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())
|
||||
val sslContext = createAndInitSslContext(keyManagerFactory, trustManagerFactory)
|
||||
val sslEngine = sslContext.createSSLEngine()
|
||||
sslEngine.useClientMode = false
|
||||
sslEngine.needClientAuth = true
|
||||
@ -256,6 +250,15 @@ internal fun createServerSslHandler(keyStore: CertificateStore,
|
||||
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
|
||||
fun initialiseTrustStoreAndEnableCrlChecking(trustStore: CertificateStore, revocationConfig: RevocationConfig): ManagerFactoryParameters {
|
||||
val pkixParams = PKIXBuilderParameters(trustStore.value.internal, X509CertSelector())
|
||||
|
@ -14,7 +14,7 @@ class HibernateConfigurationFactoryLoadingTest {
|
||||
val cacheFactory = mock<NamedCacheFactory>()
|
||||
HibernateConfiguration(
|
||||
emptySet(),
|
||||
DatabaseConfig(),
|
||||
false,
|
||||
emptyList(),
|
||||
jdbcUrl,
|
||||
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,
|
||||
inMemoryDB = false, // Ensure database is persisted between node restarts so we can keep suspended flows
|
||||
cordappsForAllNodes = emptyList(),
|
||||
notarySpecs = emptyList()
|
||||
notarySpecs = emptyList(),
|
||||
allowHibernateToManageAppSchema = false
|
||||
)) {
|
||||
createSuspendedFlowInBob()
|
||||
val cordappsDir = baseDirectory(BOB_NAME) / "cordapps"
|
||||
|
@ -86,7 +86,7 @@ class NodeStatePersistenceTests {
|
||||
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 page = it.proxy.vaultQuery(MessageState::class.java)
|
||||
page.states.singleOrNull()
|
||||
|
@ -47,7 +47,7 @@ class DistributedServiceTests {
|
||||
invokeRpc(CordaRPCOps::stateMachinesFeed))
|
||||
)
|
||||
driver(DriverParameters(
|
||||
cordappsForAllNodes = FINANCE_CORDAPPS + cordappWithPackages("net.corda.notary.raft"),
|
||||
cordappsForAllNodes = FINANCE_CORDAPPS + cordappWithPackages(),
|
||||
notarySpecs = listOf(NotarySpec(
|
||||
DUMMY_NOTARY_NAME,
|
||||
rpcUsers = listOf(testUser),
|
||||
|
@ -84,7 +84,8 @@ class NodeRegistrationTest {
|
||||
portAllocation = portAllocation,
|
||||
compatibilityZone = compatibilityZone,
|
||||
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()
|
||||
|
||||
|
@ -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;
|
||||
|
||||
import com.google.common.io.LineProcessor;
|
||||
import net.corda.client.rpc.CordaRPCClient;
|
||||
import net.corda.core.concurrent.CordaFuture;
|
||||
import net.corda.node.services.Permissions;
|
||||
|
@ -44,7 +44,7 @@ class BootTests {
|
||||
rpc.startFlow(::ObjectInputStreamFlow).returnValue.getOrThrow()
|
||||
}
|
||||
}
|
||||
driver(DriverParameters(cordappsForAllNodes = listOf(enclosedCordapp()))) {
|
||||
driver(DriverParameters(cordappsForAllNodes = listOf(enclosedCordapp()), allowHibernateToManageAppSchema = false)) {
|
||||
val devModeNode = startNode(devParams).getOrThrow()
|
||||
val node = startNode(ALICE_NAME, devMode = false, parameters = params).getOrThrow()
|
||||
|
||||
|
@ -15,6 +15,7 @@ import net.corda.testing.core.singleIdentity
|
||||
import net.corda.testing.driver.DriverParameters
|
||||
import net.corda.testing.driver.driver
|
||||
import net.corda.testing.node.User
|
||||
import net.corda.testing.node.internal.enclosedCordapp
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
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`() {
|
||||
val user = User("u", "p", setOf(startFlow<ReceiveFlow>()))
|
||||
// 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(
|
||||
startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)),
|
||||
startNode(providedName = BOB_NAME)).transpose().getOrThrow()
|
||||
|
@ -17,7 +17,7 @@ import javax.security.auth.x500.X500Principal
|
||||
class NodeKeystoreCheckTest {
|
||||
@Test(timeout=300_000)
|
||||
fun `starting node in non-dev mode with no key store`() {
|
||||
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = emptyList())) {
|
||||
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = emptyList(), allowHibernateToManageAppSchema = false)) {
|
||||
assertThatThrownBy {
|
||||
startNode(customOverrides = mapOf("devMode" to false)).getOrThrow()
|
||||
}.hasMessageContaining("One or more keyStores (identity or TLS) or trustStore not found.")
|
||||
@ -26,7 +26,7 @@ class NodeKeystoreCheckTest {
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `node should throw exception if cert path does not chain to the trust root`() {
|
||||
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = emptyList())) {
|
||||
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = emptyList(), allowHibernateToManageAppSchema = false)) {
|
||||
// Create keystores.
|
||||
val keystorePassword = "password"
|
||||
val certificatesDirectory = baseDirectory(ALICE_NAME) / "certificates"
|
||||
|
@ -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 org.junit.Test
|
||||
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.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class FlowReloadAfterCheckpointTest {
|
||||
|
||||
@ -46,9 +48,9 @@ class FlowReloadAfterCheckpointTest {
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
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 ->
|
||||
reloadCounts.compute(id) { _, value -> value?.plus(1) ?: 1 }
|
||||
reloads.add(id)
|
||||
}
|
||||
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 flowStartedByAlice = handle.id
|
||||
handle.returnValue.getOrThrow()
|
||||
assertEquals(5, reloadCounts[flowStartedByAlice])
|
||||
assertEquals(6, reloadCounts[ReloadFromCheckpointResponder.flowId])
|
||||
assertEquals(5, reloads.filter { it == flowStartedByAlice }.count())
|
||||
assertEquals(6, reloads.filter { it == ReloadFromCheckpointResponder.flowId }.count())
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
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 ->
|
||||
reloadCounts.compute(id) { _, value -> value?.plus(1) ?: 1 }
|
||||
reloads.add(id)
|
||||
}
|
||||
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = cordapps)) {
|
||||
|
||||
@ -89,24 +91,22 @@ class FlowReloadAfterCheckpointTest {
|
||||
.getOrThrow()
|
||||
|
||||
val handle = alice.rpc.startFlow(::ReloadFromCheckpointFlow, bob.nodeInfo.singleIdentity(), false, false, false)
|
||||
val flowStartedByAlice = handle.id
|
||||
handle.returnValue.getOrThrow()
|
||||
assertNull(reloadCounts[flowStartedByAlice])
|
||||
assertNull(reloadCounts[ReloadFromCheckpointResponder.flowId])
|
||||
assertEquals(0, reloads.size)
|
||||
}
|
||||
}
|
||||
|
||||
@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`() {
|
||||
val reloadCounts = mutableMapOf<StateMachineRunId, Int>()
|
||||
val reloads = ConcurrentLinkedQueue<StateMachineRunId>()
|
||||
FlowStateMachineImpl.onReloadFlowFromCheckpoint = { id ->
|
||||
reloadCounts.compute(id) { _, value -> value?.plus(1) ?: 1 }
|
||||
reloads.add(id)
|
||||
}
|
||||
lateinit var flowKeptForObservation: StateMachineRunId
|
||||
val lock = Semaphore(0)
|
||||
val lock = CountDownLatch(1)
|
||||
StaffedFlowHospital.onFlowKeptForOvernightObservation.add { id, _ ->
|
||||
flowKeptForObservation = id
|
||||
lock.release()
|
||||
lock.countDown()
|
||||
}
|
||||
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 flowStartedByAlice = handle.id
|
||||
lock.acquire()
|
||||
lock.await()
|
||||
assertEquals(flowStartedByAlice, flowKeptForObservation)
|
||||
assertEquals(4, reloadCounts[flowStartedByAlice])
|
||||
assertEquals(4, reloadCounts[ReloadFromCheckpointResponder.flowId])
|
||||
assertEquals(4, reloads.filter { it == flowStartedByAlice }.count())
|
||||
assertEquals(4, reloads.filter { it == ReloadFromCheckpointResponder.flowId }.count())
|
||||
}
|
||||
}
|
||||
|
||||
@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`() {
|
||||
val reloadCounts = mutableMapOf<StateMachineRunId, Int>()
|
||||
val reloads = ConcurrentLinkedQueue<StateMachineRunId>()
|
||||
FlowStateMachineImpl.onReloadFlowFromCheckpoint = { id ->
|
||||
reloadCounts.compute(id) { _, value -> value?.plus(1) ?: 1 }
|
||||
reloads.add(id)
|
||||
}
|
||||
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 flowStartedByAlice = handle.id
|
||||
handle.returnValue.getOrThrow()
|
||||
assertEquals(5, reloadCounts[flowStartedByAlice])
|
||||
assertEquals(6, reloadCounts[ReloadFromCheckpointResponder.flowId])
|
||||
assertEquals(5, reloads.filter { it == flowStartedByAlice }.count())
|
||||
assertEquals(6, reloads.filter { it == ReloadFromCheckpointResponder.flowId }.count())
|
||||
}
|
||||
}
|
||||
|
||||
@ -189,8 +189,8 @@ class FlowReloadAfterCheckpointTest {
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `timed flow will reload from initial checkpoint after calling a suspending function when reloadCheckpointAfterSuspend is true`() {
|
||||
var reloadCount = 0
|
||||
FlowStateMachineImpl.onReloadFlowFromCheckpoint = { _ -> reloadCount += 1 }
|
||||
val reloads = ConcurrentLinkedQueue<StateMachineRunId>()
|
||||
FlowStateMachineImpl.onReloadFlowFromCheckpoint = { runId -> reloads.add(runId) }
|
||||
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = cordapps)) {
|
||||
|
||||
val alice = startNode(
|
||||
@ -199,14 +199,14 @@ class FlowReloadAfterCheckpointTest {
|
||||
).getOrThrow()
|
||||
|
||||
alice.rpc.startFlow(::MyTimedFlow).returnValue.getOrThrow()
|
||||
assertEquals(5, reloadCount)
|
||||
assertEquals(5, reloads.size)
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `flow will correctly retry after an error when reloadCheckpointAfterSuspend is true`() {
|
||||
var reloadCount = 0
|
||||
FlowStateMachineImpl.onReloadFlowFromCheckpoint = { _ -> reloadCount += 1 }
|
||||
val reloads = ConcurrentLinkedQueue<StateMachineRunId>()
|
||||
FlowStateMachineImpl.onReloadFlowFromCheckpoint = { runId -> reloads.add(runId) }
|
||||
var timesDischarged = 0
|
||||
StaffedFlowHospital.onFlowDischarged.add { _, _ -> timesDischarged += 1 }
|
||||
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = cordapps)) {
|
||||
@ -217,15 +217,21 @@ class FlowReloadAfterCheckpointTest {
|
||||
).getOrThrow()
|
||||
|
||||
alice.rpc.startFlow(::TransientConnectionFailureFlow).returnValue.getOrThrow()
|
||||
assertEquals(5, reloadCount)
|
||||
assertEquals(5, reloads.size)
|
||||
assertEquals(3, timesDischarged)
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `flow continues reloading from checkpoints after node restart when reloadCheckpointAfterSuspend is true`() {
|
||||
var reloadCount = 0
|
||||
FlowStateMachineImpl.onReloadFlowFromCheckpoint = { _ -> reloadCount += 1 }
|
||||
val reloads = ConcurrentLinkedQueue<StateMachineRunId>()
|
||||
val firstLatch = CountDownLatch(2)
|
||||
val secondLatch = CountDownLatch(5)
|
||||
FlowStateMachineImpl.onReloadFlowFromCheckpoint = { runId ->
|
||||
reloads.add(runId)
|
||||
firstLatch.countDown()
|
||||
secondLatch.countDown()
|
||||
}
|
||||
driver(
|
||||
DriverParameters(
|
||||
inMemoryDB = false,
|
||||
@ -241,25 +247,31 @@ class FlowReloadAfterCheckpointTest {
|
||||
).getOrThrow()
|
||||
|
||||
alice.rpc.startFlow(::MyHospitalizingFlow)
|
||||
Thread.sleep(10.seconds.toMillis())
|
||||
|
||||
assertTrue { firstLatch.await(10, TimeUnit.SECONDS) }
|
||||
alice.stop()
|
||||
assertEquals(2, reloads.size)
|
||||
|
||||
// Set up a new latch
|
||||
startNode(
|
||||
providedName = ALICE_NAME,
|
||||
customOverrides = mapOf(NodeConfiguration::reloadCheckpointAfterSuspend.name to true)
|
||||
).getOrThrow()
|
||||
|
||||
Thread.sleep(20.seconds.toMillis())
|
||||
|
||||
assertEquals(5, reloadCount)
|
||||
assertTrue { secondLatch.await(20, TimeUnit.SECONDS) }
|
||||
assertEquals(5, reloads.size)
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `idempotent flow continues reloading from checkpoints after node restart when reloadCheckpointAfterSuspend is true`() {
|
||||
var reloadCount = 0
|
||||
FlowStateMachineImpl.onReloadFlowFromCheckpoint = { _ -> reloadCount += 1 }
|
||||
// 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
|
||||
val reloadsExpected = CountDownLatch(7)
|
||||
val reloads = ConcurrentLinkedQueue<StateMachineRunId>()
|
||||
FlowStateMachineImpl.onReloadFlowFromCheckpoint = { runId ->
|
||||
reloads.add(runId)
|
||||
reloadsExpected.countDown()
|
||||
}
|
||||
driver(
|
||||
DriverParameters(
|
||||
inMemoryDB = false,
|
||||
@ -284,19 +296,18 @@ class FlowReloadAfterCheckpointTest {
|
||||
customOverrides = mapOf(NodeConfiguration::reloadCheckpointAfterSuspend.name to true)
|
||||
).getOrThrow()
|
||||
|
||||
Thread.sleep(20.seconds.toMillis())
|
||||
|
||||
// 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
|
||||
assertEquals(7, reloadCount)
|
||||
assertTrue { reloadsExpected.await(20, TimeUnit.SECONDS) }
|
||||
assertEquals(7, reloads.size)
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
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 ->
|
||||
reloadCounts.compute(id) { _, value -> value?.plus(1) ?: 1 }
|
||||
reloads.add(id)
|
||||
}
|
||||
driver(DriverParameters(startNodesInProcess = true, cordappsForAllNodes = FINANCE_CORDAPPS)) {
|
||||
|
||||
@ -325,8 +336,8 @@ class FlowReloadAfterCheckpointTest {
|
||||
.toSet()
|
||||
.single()
|
||||
Thread.sleep(10.seconds.toMillis())
|
||||
assertEquals(7, reloadCounts[flowStartedByAlice])
|
||||
assertEquals(6, reloadCounts[flowStartedByBob])
|
||||
assertEquals(7, reloads.filter { it == flowStartedByAlice }.size)
|
||||
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.node.internal.FINANCE_CORDAPPS
|
||||
import org.assertj.core.api.Assertions
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
import java.time.Duration
|
||||
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)
|
||||
fun `a killed flow will propagate the killed error to counter parties if it was suspended`() {
|
||||
driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) {
|
||||
|
@ -2,32 +2,21 @@ package net.corda.node.persistence
|
||||
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.node.flows.isQuasarAgentSpecified
|
||||
import net.corda.nodeapi.internal.persistence.DatabaseIncompatibleException
|
||||
import net.corda.node.internal.ConfigurationException
|
||||
import net.corda.testing.driver.DriverParameters
|
||||
import net.corda.testing.driver.NodeParameters
|
||||
import net.corda.testing.driver.driver
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertNotNull
|
||||
|
||||
class DbSchemaInitialisationTest {
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `database is initialised`() {
|
||||
@Test(timeout = 300_000)
|
||||
fun `database initialisation not allowed in config`() {
|
||||
driver(DriverParameters(startNodesInProcess = isQuasarAgentSpecified(), cordappsForAllNodes = emptyList())) {
|
||||
val nodeHandle = {
|
||||
startNode(NodeParameters(customOverrides = mapOf("database.initialiseSchema" to "true"))).getOrThrow()
|
||||
}()
|
||||
assertNotNull(nodeHandle)
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `database is not initialised`() {
|
||||
driver(DriverParameters(startNodesInProcess = isQuasarAgentSpecified(), cordappsForAllNodes = emptyList())) {
|
||||
assertFailsWith(DatabaseIncompatibleException::class) {
|
||||
assertFailsWith(ConfigurationException::class) {
|
||||
startNode(NodeParameters(customOverrides = mapOf("database.initialiseSchema" to "false"))).getOrThrow()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,8 +1,10 @@
|
||||
package net.corda.node.services.network
|
||||
|
||||
import net.corda.core.crypto.random63BitValue
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.*
|
||||
import net.corda.core.messaging.ParametersUpdateInfo
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.serialization.serialize
|
||||
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_UPDATE_FILE_NAME
|
||||
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.testNetworkParameters
|
||||
import net.corda.testing.core.*
|
||||
@ -74,7 +77,6 @@ class NetworkMapTest(var initFunc: (URL, NetworkMapServer) -> CompatibilityZoneP
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@Before
|
||||
fun start() {
|
||||
networkMapServer = NetworkMapServer(cacheTimeout, portAllocation.nextHostAndPort())
|
||||
@ -92,7 +94,8 @@ class NetworkMapTest(var initFunc: (URL, NetworkMapServer) -> CompatibilityZoneP
|
||||
internalDriver(
|
||||
portAllocation = portAllocation,
|
||||
compatibilityZone = compatibilityZone,
|
||||
notarySpecs = emptyList()
|
||||
notarySpecs = emptyList(),
|
||||
allowHibernateToManageAppSchema = false
|
||||
) {
|
||||
val alice = startNode(providedName = ALICE_NAME, devMode = false).getOrThrow() as NodeHandleInternal
|
||||
val nextParams = networkMapServer.networkParameters.copy(
|
||||
@ -141,31 +144,136 @@ 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)
|
||||
fun `nodes process additions and removals from the network map correctly (and also download the network parameters)`() {
|
||||
internalDriver(
|
||||
portAllocation = portAllocation,
|
||||
compatibilityZone = compatibilityZone,
|
||||
notarySpecs = emptyList()
|
||||
notarySpecs = emptyList(),
|
||||
allowHibernateToManageAppSchema = false
|
||||
) {
|
||||
val aliceNode = startNode(providedName = ALICE_NAME, devMode = false).getOrThrow()
|
||||
assertDownloadedNetworkParameters(aliceNode)
|
||||
aliceNode.onlySees(aliceNode.nodeInfo)
|
||||
startNode(providedName = ALICE_NAME, devMode = false).getOrThrow().use { aliceNode ->
|
||||
assertDownloadedNetworkParameters(aliceNode)
|
||||
aliceNode.onlySees(aliceNode.nodeInfo)
|
||||
|
||||
val bobNode = startNode(providedName = BOB_NAME, devMode = false).getOrThrow()
|
||||
// Wait for network map client to poll for the next update.
|
||||
Thread.sleep(cacheTimeout.toMillis() * 2)
|
||||
|
||||
// Wait for network map client to poll for the next update.
|
||||
Thread.sleep(cacheTimeout.toMillis() * 2)
|
||||
startNode(providedName = BOB_NAME, devMode = false).getOrThrow().use { bobNode ->
|
||||
bobNode.onlySees(aliceNode.nodeInfo, bobNode.nodeInfo)
|
||||
aliceNode.onlySees(aliceNode.nodeInfo, bobNode.nodeInfo)
|
||||
|
||||
bobNode.onlySees(aliceNode.nodeInfo, bobNode.nodeInfo)
|
||||
aliceNode.onlySees(aliceNode.nodeInfo, bobNode.nodeInfo)
|
||||
networkMapServer.removeNodeInfo(aliceNode.nodeInfo)
|
||||
|
||||
networkMapServer.removeNodeInfo(aliceNode.nodeInfo)
|
||||
// Wait for network map client to poll for the next update.
|
||||
Thread.sleep(cacheTimeout.toMillis() * 2)
|
||||
|
||||
// Wait for network map client to poll for the next update.
|
||||
Thread.sleep(cacheTimeout.toMillis() * 2)
|
||||
|
||||
bobNode.onlySees(bobNode.nodeInfo)
|
||||
bobNode.onlySees(bobNode.nodeInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -175,26 +283,28 @@ class NetworkMapTest(var initFunc: (URL, NetworkMapServer) -> CompatibilityZoneP
|
||||
portAllocation = portAllocation,
|
||||
compatibilityZone = compatibilityZone,
|
||||
notarySpecs = emptyList(),
|
||||
systemProperties = mapOf("net.corda.node.internal.nodeinfo.publish.interval" to 1.seconds.toString())
|
||||
systemProperties = mapOf("net.corda.node.internal.nodeinfo.publish.interval" to 1.seconds.toString()),
|
||||
allowHibernateToManageAppSchema = false
|
||||
) {
|
||||
val aliceNode = startNode(providedName = ALICE_NAME, devMode = false).getOrThrow()
|
||||
val aliceNodeInfo = aliceNode.nodeInfo.serialize().hash
|
||||
assertThat(networkMapServer.networkMapHashes()).contains(aliceNodeInfo)
|
||||
networkMapServer.removeNodeInfo(aliceNode.nodeInfo)
|
||||
|
||||
var maxRemoveRetries = 5
|
||||
|
||||
// Try to remove multiple times in case the network map republishes just in between the removal and the check.
|
||||
while (aliceNodeInfo in networkMapServer.networkMapHashes()) {
|
||||
startNode(providedName = ALICE_NAME, devMode = false).getOrThrow().use { aliceNode ->
|
||||
val aliceNodeInfo = aliceNode.nodeInfo.serialize().hash
|
||||
assertThat(networkMapServer.networkMapHashes()).contains(aliceNodeInfo)
|
||||
networkMapServer.removeNodeInfo(aliceNode.nodeInfo)
|
||||
if (maxRemoveRetries-- == 0) {
|
||||
throw AssertionError("Could not remove Node info.")
|
||||
}
|
||||
}
|
||||
|
||||
// Wait until the node info is republished.
|
||||
Thread.sleep(2000)
|
||||
assertThat(networkMapServer.networkMapHashes()).contains(aliceNodeInfo)
|
||||
var maxRemoveRetries = 5
|
||||
|
||||
// Try to remove multiple times in case the network map republishes just in between the removal and the check.
|
||||
while (aliceNodeInfo in networkMapServer.networkMapHashes()) {
|
||||
networkMapServer.removeNodeInfo(aliceNode.nodeInfo)
|
||||
if (maxRemoveRetries-- == 0) {
|
||||
throw AssertionError("Could not remove Node info.")
|
||||
}
|
||||
}
|
||||
|
||||
// Wait until the node info is republished.
|
||||
Thread.sleep(2000)
|
||||
assertThat(networkMapServer.networkMapHashes()).contains(aliceNodeInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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),
|
||||
startNode(ALICE_NAME, devMode = false, parameters = params))
|
||||
.transpose()
|
||||
@ -79,7 +79,7 @@ class RpcExceptionHandlingTest {
|
||||
rpc.startFlow(::FlowExceptionFlow, expectedMessage, expectedErrorId).returnValue.getOrThrow()
|
||||
}
|
||||
|
||||
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = listOf(enclosedCordapp()))) {
|
||||
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = listOf(enclosedCordapp()), allowHibernateToManageAppSchema = false)) {
|
||||
val (devModeNode, node) = listOf(startNode(params, BOB_NAME),
|
||||
startNode(ALICE_NAME, devMode = false, parameters = params))
|
||||
.transpose()
|
||||
@ -115,7 +115,7 @@ class RpcExceptionHandlingTest {
|
||||
nodeA.rpc.startFlow(::InitFlow, nodeB.nodeInfo.singleIdentity()).returnValue.getOrThrow()
|
||||
}
|
||||
|
||||
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = listOf(enclosedCordapp()))) {
|
||||
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = listOf(enclosedCordapp()), allowHibernateToManageAppSchema = false)) {
|
||||
|
||||
assertThatThrownBy { scenario(ALICE_NAME, BOB_NAME,true) }.isInstanceOfSatisfying(CordaRuntimeException::class.java) { exception ->
|
||||
|
||||
|
@ -33,13 +33,19 @@ class FlowVersioningTest : NodeBasedTest() {
|
||||
private class PretendInitiatingCoreFlow(val initiatedParty: Party) : FlowLogic<Pair<Int, Int>>() {
|
||||
@Suspendable
|
||||
override fun call(): Pair<Int, Int> {
|
||||
// Execute receive() outside of the Pair constructor to avoid Kotlin/Quasar instrumentation bug.
|
||||
val session = initiateFlow(initiatedParty)
|
||||
val alicePlatformVersionAccordingToBob = session.receive<Int>().unwrap { it }
|
||||
return Pair(
|
||||
alicePlatformVersionAccordingToBob,
|
||||
session.getCounterpartyFlowInfo().flowVersion
|
||||
)
|
||||
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 }
|
||||
Pair(
|
||||
alicePlatformVersionAccordingToBob,
|
||||
bobPlatformVersionAccordingToAlice
|
||||
)
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -445,11 +445,11 @@ class VaultObserverExceptionTest {
|
||||
|
||||
val user = User("user", "foo", setOf(Permissions.all()))
|
||||
driver(DriverParameters(startNodesInProcess = true,
|
||||
cordappsForAllNodes = listOf(
|
||||
findCordapp("com.r3.dbfailure.contracts"),
|
||||
findCordapp("com.r3.dbfailure.workflows"),
|
||||
findCordapp("com.r3.dbfailure.schemas")
|
||||
),inMemoryDB = false)
|
||||
cordappsForAllNodes = listOf(
|
||||
findCordapp("com.r3.dbfailure.contracts"),
|
||||
findCordapp("com.r3.dbfailure.workflows"),
|
||||
findCordapp("com.r3.dbfailure.schemas")
|
||||
), inMemoryDB = false)
|
||||
) {
|
||||
val (aliceNode, bobNode) = listOf(ALICE_NAME, BOB_NAME)
|
||||
.map { startNode(providedName = it,
|
||||
@ -537,12 +537,12 @@ class VaultObserverExceptionTest {
|
||||
|
||||
val user = User("user", "foo", setOf(Permissions.all()))
|
||||
driver(DriverParameters(startNodesInProcess = true,
|
||||
cordappsForAllNodes = listOf(
|
||||
findCordapp("com.r3.dbfailure.contracts"),
|
||||
findCordapp("com.r3.dbfailure.workflows"),
|
||||
findCordapp("com.r3.dbfailure.schemas")
|
||||
),
|
||||
inMemoryDB = false)
|
||||
cordappsForAllNodes = listOf(
|
||||
findCordapp("com.r3.dbfailure.contracts"),
|
||||
findCordapp("com.r3.dbfailure.workflows"),
|
||||
findCordapp("com.r3.dbfailure.schemas")
|
||||
),
|
||||
inMemoryDB = false)
|
||||
) {
|
||||
val (aliceNode, bobNode) = listOf(ALICE_NAME, BOB_NAME)
|
||||
.map { startNode(providedName = it,
|
||||
@ -622,12 +622,12 @@ class VaultObserverExceptionTest {
|
||||
|
||||
val user = User("user", "foo", setOf(Permissions.all()))
|
||||
driver(DriverParameters(startNodesInProcess = true,
|
||||
cordappsForAllNodes = listOf(
|
||||
findCordapp("com.r3.dbfailure.contracts"),
|
||||
findCordapp("com.r3.dbfailure.workflows"),
|
||||
findCordapp("com.r3.dbfailure.schemas")
|
||||
),
|
||||
inMemoryDB = false)
|
||||
cordappsForAllNodes = listOf(
|
||||
findCordapp("com.r3.dbfailure.contracts"),
|
||||
findCordapp("com.r3.dbfailure.workflows"),
|
||||
findCordapp("com.r3.dbfailure.schemas")
|
||||
),
|
||||
inMemoryDB = false)
|
||||
) {
|
||||
val (aliceNode, bobNode) = listOf(ALICE_NAME, BOB_NAME)
|
||||
.map { startNode(providedName = it,
|
||||
@ -702,12 +702,12 @@ class VaultObserverExceptionTest {
|
||||
|
||||
val user = User("user", "foo", setOf(Permissions.all()))
|
||||
driver(DriverParameters(startNodesInProcess = true,
|
||||
cordappsForAllNodes = listOf(
|
||||
findCordapp("com.r3.dbfailure.contracts"),
|
||||
findCordapp("com.r3.dbfailure.workflows"),
|
||||
findCordapp("com.r3.dbfailure.schemas")
|
||||
),
|
||||
inMemoryDB = false)
|
||||
cordappsForAllNodes = listOf(
|
||||
findCordapp("com.r3.dbfailure.contracts"),
|
||||
findCordapp("com.r3.dbfailure.workflows"),
|
||||
findCordapp("com.r3.dbfailure.schemas")
|
||||
),
|
||||
inMemoryDB = false)
|
||||
) {
|
||||
val (aliceNode, bobNode) = listOf(ALICE_NAME, BOB_NAME)
|
||||
.map { startNode(providedName = it,
|
||||
@ -762,12 +762,12 @@ class VaultObserverExceptionTest {
|
||||
fun `Accessing NodeVaultService rawUpdates from a flow is not allowed` () {
|
||||
val user = User("user", "foo", setOf(Permissions.all()))
|
||||
driver(DriverParameters(startNodesInProcess = true,
|
||||
cordappsForAllNodes = listOf(
|
||||
findCordapp("com.r3.dbfailure.contracts"),
|
||||
findCordapp("com.r3.dbfailure.workflows"),
|
||||
findCordapp("com.r3.dbfailure.schemas")
|
||||
),
|
||||
inMemoryDB = false)
|
||||
cordappsForAllNodes = listOf(
|
||||
findCordapp("com.r3.dbfailure.contracts"),
|
||||
findCordapp("com.r3.dbfailure.workflows"),
|
||||
findCordapp("com.r3.dbfailure.schemas")
|
||||
),
|
||||
inMemoryDB = false)
|
||||
) {
|
||||
val aliceNode = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow()
|
||||
|
||||
@ -792,12 +792,12 @@ class VaultObserverExceptionTest {
|
||||
|
||||
val user = User("user", "foo", setOf(Permissions.all()))
|
||||
driver(DriverParameters(startNodesInProcess = true,
|
||||
cordappsForAllNodes = listOf(
|
||||
findCordapp("com.r3.dbfailure.contracts"),
|
||||
findCordapp("com.r3.dbfailure.workflows"),
|
||||
findCordapp("com.r3.transactionfailure.workflows"),
|
||||
findCordapp("com.r3.dbfailure.schemas")),
|
||||
inMemoryDB = false)
|
||||
cordappsForAllNodes = listOf(
|
||||
findCordapp("com.r3.dbfailure.contracts"),
|
||||
findCordapp("com.r3.dbfailure.workflows"),
|
||||
findCordapp("com.r3.transactionfailure.workflows"),
|
||||
findCordapp("com.r3.dbfailure.schemas")),
|
||||
inMemoryDB = false)
|
||||
) {
|
||||
val aliceNode = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow()
|
||||
|
||||
@ -823,12 +823,12 @@ class VaultObserverExceptionTest {
|
||||
|
||||
val user = User("user", "foo", setOf(Permissions.all()))
|
||||
driver(DriverParameters(startNodesInProcess = true,
|
||||
cordappsForAllNodes = listOf(
|
||||
findCordapp("com.r3.dbfailure.contracts"),
|
||||
findCordapp("com.r3.dbfailure.workflows"),
|
||||
findCordapp("com.r3.transactionfailure.workflows"),
|
||||
findCordapp("com.r3.dbfailure.schemas")),
|
||||
inMemoryDB = false)
|
||||
cordappsForAllNodes = listOf(
|
||||
findCordapp("com.r3.dbfailure.contracts"),
|
||||
findCordapp("com.r3.dbfailure.workflows"),
|
||||
findCordapp("com.r3.transactionfailure.workflows"),
|
||||
findCordapp("com.r3.dbfailure.schemas")),
|
||||
inMemoryDB = false)
|
||||
) {
|
||||
// Subscribing with custom SafeSubscriber; the custom SafeSubscriber will not get replaced by a ResilientSubscriber
|
||||
// meaning that it will behave as a SafeSubscriber; it will get unsubscribed upon throwing an error.
|
||||
|
@ -42,7 +42,6 @@ class P2PMessagingTest {
|
||||
private fun startDriverWithDistributedService(dsl: DriverDSL.(List<InProcess>) -> Unit) {
|
||||
driver(DriverParameters(
|
||||
startNodesInProcess = true,
|
||||
extraCordappPackagesToScan = listOf("net.corda.notary.raft"),
|
||||
notarySpecs = listOf(NotarySpec(DISTRIBUTED_SERVICE_NAME, cluster = ClusterSpec.Raft(clusterSize = 2)))
|
||||
)) {
|
||||
dsl(defaultNotaryHandle.nodeHandles.getOrThrow().map { (it as InProcess) })
|
||||
|
@ -48,6 +48,14 @@ open class SharedNodeCmdLineOptions {
|
||||
)
|
||||
var devMode: Boolean? = null
|
||||
|
||||
@Option(
|
||||
names = ["--allow-hibernate-to-manage-app-schema"],
|
||||
description = ["Allows hibernate to create/modify app schema for CorDapps based on their mapped schema.",
|
||||
"Use this for rapid app development or for compatibility with pre-4.6 CorDapps.",
|
||||
"Only available in dev mode."]
|
||||
)
|
||||
var allowHibernateToManageAppSchema: Boolean = false
|
||||
|
||||
open fun parseConfiguration(configuration: Config): Valid<NodeConfiguration> {
|
||||
val option = Configuration.Options(strict = unknownConfigKeysPolicy == UnknownConfigKeysPolicy.FAIL)
|
||||
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.network.NetworkMapClient
|
||||
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.PersistentNetworkMapCache
|
||||
import net.corda.node.services.persistence.AbstractPartyDescriptor
|
||||
@ -176,7 +178,6 @@ import org.slf4j.Logger
|
||||
import rx.Scheduler
|
||||
import java.io.IOException
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
import java.nio.file.Path
|
||||
import java.security.KeyPair
|
||||
import java.security.KeyStoreException
|
||||
import java.security.cert.X509Certificate
|
||||
@ -185,7 +186,7 @@ import java.sql.Savepoint
|
||||
import java.time.Clock
|
||||
import java.time.Duration
|
||||
import java.time.format.DateTimeParseException
|
||||
import java.util.Properties
|
||||
import java.util.*
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.LinkedBlockingQueue
|
||||
@ -195,6 +196,8 @@ import java.util.concurrent.TimeUnit.MINUTES
|
||||
import java.util.concurrent.TimeUnit.SECONDS
|
||||
import java.util.function.Consumer
|
||||
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
|
||||
@ -212,9 +215,12 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
val serverThread: AffinityExecutor.ServiceAffinityExecutor,
|
||||
val busyNodeLatch: ReusableLatch = ReusableLatch(),
|
||||
djvmBootstrapSource: ApiSource = EmptyApi,
|
||||
djvmCordaSource: UserSource? = null) : SingletonSerializeAsToken() {
|
||||
djvmCordaSource: UserSource? = null,
|
||||
protected val allowHibernateToManageAppSchema: Boolean = false,
|
||||
private val allowAppSchemaUpgradeWithCheckpoints: Boolean = false) : SingletonSerializeAsToken() {
|
||||
|
||||
protected abstract val log: Logger
|
||||
|
||||
@Suppress("LeakingThis")
|
||||
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 open val runMigrationScripts: Boolean = configuredDbIsInMemory()
|
||||
|
||||
// if the configured DB is in memory, we will need to run db migrations, as the db does not persist between runs.
|
||||
private fun configuredDbIsInMemory() = configuration.dataSourceProperties.getProperty("dataSource.url").startsWith("jdbc:h2:mem:")
|
||||
|
||||
init {
|
||||
(serverThread as? ExecutorService)?.let {
|
||||
runOnStop += {
|
||||
@ -235,6 +246,12 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
}
|
||||
|
||||
quasarExcludePackages(configuration)
|
||||
|
||||
if (allowHibernateToManageAppSchema && !configuration.devMode) {
|
||||
throw ConfigurationException("Hibernate can only be used to manage app schema in development while using dev mode. " +
|
||||
"Please remove the --allow-hibernate-to-manage-app-schema command line flag and provide schema migration scripts for your CorDapps."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val notaryLoader = configuration.notary?.let {
|
||||
@ -250,7 +267,8 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
schemaService,
|
||||
configuration.dataSourceProperties,
|
||||
cacheFactory,
|
||||
cordappLoader.appClassLoader)
|
||||
cordappLoader.appClassLoader,
|
||||
allowHibernateToManageAppSchema)
|
||||
|
||||
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 {
|
||||
check(started == null) { "Node has already been started" }
|
||||
|
||||
@ -486,7 +552,8 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
startShell()
|
||||
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")
|
||||
check(netParams.minimumPlatformVersion <= versionInfo.platformVersion) {
|
||||
"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
|
||||
identityService.ourNames = nodeInfo.legalIdentities.map { it.name }.toSet()
|
||||
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(
|
||||
trustRoot,
|
||||
signedNetParams.raw.hash,
|
||||
signedNodeInfo,
|
||||
netParams,
|
||||
keyManagementService,
|
||||
configuration.networkParameterAcceptanceSettings!!)
|
||||
configuration.networkParameterAcceptanceSettings!!,
|
||||
networkParametersHotloader)
|
||||
|
||||
try {
|
||||
startMessagingService(rpcOps, nodeInfo, myNotaryIdentity, netParams)
|
||||
} catch (e: Exception) {
|
||||
@ -956,7 +1037,12 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
protected open fun startDatabase() {
|
||||
val props = configuration.dataSourceProperties
|
||||
if (props.isEmpty) throw DatabaseConfigurationException("There must be a database configured.")
|
||||
database.startHikariPool(props, configuration.database, schemaService.internalSchemas(), metricRegistry, this.cordappLoader, configuration.baseDirectory, configuration.myLegalName)
|
||||
database.startHikariPool(props, 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.
|
||||
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 stateMachineRecordedTransactionMapping = DBTransactionMappingStorage(database)
|
||||
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
|
||||
|
||||
@Volatile
|
||||
private lateinit var _networkParameters: NetworkParameters
|
||||
override val networkParameters: NetworkParameters get() = _networkParameters
|
||||
|
||||
@ -1272,6 +1359,10 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
val ledgerTransaction = servicesForResolution.specialise(ltx)
|
||||
return verifierFactoryService.apply(ledgerTransaction)
|
||||
}
|
||||
|
||||
override fun onNewNetworkParameters(networkParameters: NetworkParameters) {
|
||||
this._networkParameters = networkParameters
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1338,13 +1429,15 @@ class FlowStarterImpl(
|
||||
|
||||
class ConfigurationException(message: String) : CordaException(message)
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
fun createCordaPersistence(databaseConfig: DatabaseConfig,
|
||||
wellKnownPartyFromX500Name: (CordaX500Name) -> Party?,
|
||||
wellKnownPartyFromAnonymous: (AbstractParty) -> Party?,
|
||||
schemaService: SchemaService,
|
||||
hikariProperties: Properties,
|
||||
cacheFactory: NamedCacheFactory,
|
||||
customClassLoader: ClassLoader?): CordaPersistence {
|
||||
customClassLoader: ClassLoader?,
|
||||
allowHibernateToManageAppSchema: Boolean = false): CordaPersistence {
|
||||
// Register the AbstractPartyDescriptor so Hibernate doesn't warn when encountering AbstractParty. Unfortunately
|
||||
// Hibernate warns about not being able to find a descriptor if we don't provide one, but won't use it by default
|
||||
// so we end up providing both descriptor and converter. We should re-examine this in later versions to see if
|
||||
@ -1355,25 +1448,31 @@ fun createCordaPersistence(databaseConfig: DatabaseConfig,
|
||||
|
||||
val jdbcUrl = hikariProperties.getProperty("dataSource.url", "")
|
||||
return CordaPersistence(
|
||||
databaseConfig,
|
||||
schemaService.schemas,
|
||||
jdbcUrl,
|
||||
cacheFactory,
|
||||
attributeConverters, customClassLoader,
|
||||
errorHandler = { e ->
|
||||
// "corrupting" a DatabaseTransaction only inside a flow state machine execution
|
||||
FlowStateMachineImpl.currentStateMachine()?.let {
|
||||
// register only the very first exception thrown throughout a chain of logical transactions
|
||||
setException(e)
|
||||
}
|
||||
})
|
||||
databaseConfig.exportHibernateJMXStatistics,
|
||||
schemaService.schemas,
|
||||
jdbcUrl,
|
||||
cacheFactory,
|
||||
attributeConverters, customClassLoader,
|
||||
errorHandler = { e ->
|
||||
// "corrupting" a DatabaseTransaction only inside a flow state machine execution
|
||||
FlowStateMachineImpl.currentStateMachine()?.let {
|
||||
// register only the very first exception thrown throughout a chain of logical transactions
|
||||
setException(e)
|
||||
}
|
||||
},
|
||||
allowHibernateToManageAppSchema = allowHibernateToManageAppSchema)
|
||||
}
|
||||
|
||||
fun CordaPersistence.startHikariPool(hikariProperties: Properties, databaseConfig: DatabaseConfig, schemas: Set<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 {
|
||||
val dataSource = DataSourceFactory.createDataSource(hikariProperties, metricRegistry = metricRegistry)
|
||||
val schemaMigration = SchemaMigration(schemas, dataSource, databaseConfig, cordappLoader, currentDir, ourName)
|
||||
schemaMigration.nodeStartup(dataSource.connection.use { DBCheckpointStorage.getCheckpointCount(it) != 0L })
|
||||
val haveCheckpoints = dataSource.connection.use { DBCheckpointStorage.getCheckpointCount(it) != 0L }
|
||||
|
||||
schemaMigration(dataSource, haveCheckpoints)
|
||||
start(dataSource)
|
||||
} catch (ex: Exception) {
|
||||
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? {
|
||||
|
||||
if (!nodeRpcOptions.useSsl || nodeRpcOptions.sslConfig == null) {
|
||||
|
@ -125,7 +125,8 @@ open class Node(configuration: NodeConfiguration,
|
||||
flowManager: FlowManager = NodeFlowManager(configuration.flowOverrides),
|
||||
cacheFactoryPrototype: BindableNamedCacheFactory = DefaultNamedCacheFactory(),
|
||||
djvmBootstrapSource: ApiSource = createBootstrapSource(configuration),
|
||||
djvmCordaSource: UserSource? = createCordaSource(configuration)
|
||||
djvmCordaSource: UserSource? = createCordaSource(configuration),
|
||||
allowHibernateToManageAppSchema: Boolean = false
|
||||
) : AbstractNode<NodeInfo>(
|
||||
configuration,
|
||||
createClock(configuration),
|
||||
@ -135,7 +136,8 @@ open class Node(configuration: NodeConfiguration,
|
||||
// Under normal (non-test execution) it will always be "1"
|
||||
AffinityExecutor.ServiceAffinityExecutor("Node thread-${sameVmNodeCounter.incrementAndGet()}", 1),
|
||||
djvmBootstrapSource = djvmBootstrapSource,
|
||||
djvmCordaSource = djvmCordaSource
|
||||
djvmCordaSource = djvmCordaSource,
|
||||
allowHibernateToManageAppSchema = allowHibernateToManageAppSchema
|
||||
) {
|
||||
|
||||
override fun createStartedNode(nodeInfo: NodeInfo, rpcOps: CordaRPCOps, notaryService: NotaryService?): NodeInfo =
|
||||
@ -559,6 +561,16 @@ open class Node(configuration: NodeConfiguration,
|
||||
return super.generateAndSaveNodeInfo()
|
||||
}
|
||||
|
||||
override fun runDatabaseMigrationScripts(
|
||||
updateCoreSchemas: Boolean,
|
||||
updateAppSchemas: Boolean,
|
||||
updateAppSchemasWithCheckpoints: Boolean) {
|
||||
if (allowHibernateToManageAppSchema) {
|
||||
initialiseSerialization()
|
||||
}
|
||||
super.runDatabaseMigrationScripts(updateCoreSchemas, updateAppSchemas, updateAppSchemasWithCheckpoints)
|
||||
}
|
||||
|
||||
override fun start(): NodeInfo {
|
||||
registerDefaultExceptionHandler()
|
||||
initialiseSerialization()
|
||||
|
@ -76,10 +76,18 @@ open class NodeStartupCli : CordaCliWrapper("corda", "Runs a Corda Node") {
|
||||
private val justGenerateRpcSslCertsCli by lazy { GenerateRpcSslCertsCli(startup) }
|
||||
private val initialRegistrationCli by lazy { InitialRegistrationCli(startup) }
|
||||
private val validateConfigurationCli by lazy { ValidateConfigurationCli() }
|
||||
private val runMigrationScriptsCli by lazy { RunMigrationScriptsCli(startup) }
|
||||
private val synchroniseAppSchemasCli by lazy { SynchroniseSchemasCli(startup) }
|
||||
|
||||
override fun initLogging(): Boolean = this.initLogging(cmdLineOptions.baseDirectory)
|
||||
|
||||
override fun additionalSubCommands() = setOf(networkCacheCli, justGenerateNodeInfoCli, justGenerateRpcSslCertsCli, initialRegistrationCli, validateConfigurationCli)
|
||||
override fun additionalSubCommands() = setOf(networkCacheCli,
|
||||
justGenerateNodeInfoCli,
|
||||
justGenerateRpcSslCertsCli,
|
||||
initialRegistrationCli,
|
||||
validateConfigurationCli,
|
||||
runMigrationScriptsCli,
|
||||
synchroniseAppSchemasCli)
|
||||
|
||||
override fun call(): Int {
|
||||
if (!validateBaseDirectory()) {
|
||||
@ -201,7 +209,7 @@ open class NodeStartup : NodeStartupLogging {
|
||||
|
||||
protected open fun preNetworkRegistration(conf: NodeConfiguration) = Unit
|
||||
|
||||
open fun createNode(conf: NodeConfiguration, versionInfo: VersionInfo): Node = Node(conf, versionInfo)
|
||||
open fun createNode(conf: NodeConfiguration, versionInfo: VersionInfo): Node = Node(conf, versionInfo, allowHibernateToManageAppSchema = cmdLineOptions.allowHibernateToManageAppSchema)
|
||||
|
||||
fun startNode(node: Node, startTime: Long) {
|
||||
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.node.SimpleClock
|
||||
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.DatabaseConfig
|
||||
import net.corda.nodeapi.internal.persistence.SchemaMigration.Companion.NODE_X500_NAME
|
||||
import java.io.PrintWriter
|
||||
import java.sql.Connection
|
||||
@ -74,7 +76,6 @@ abstract class CordaMigration : CustomTaskChange {
|
||||
cacheFactory: MigrationNamedCacheFactory,
|
||||
identityService: PersistentIdentityService,
|
||||
schema: Set<MappedSchema>): CordaPersistence {
|
||||
val configDefaults = DatabaseConfig()
|
||||
val attributeConverters = listOf(
|
||||
PublicKeyToTextConverter(),
|
||||
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
|
||||
// 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? {
|
||||
|
@ -24,6 +24,16 @@ interface CheckpointStorage {
|
||||
fun updateCheckpoint(id: StateMachineRunId, checkpoint: Checkpoint, serializedFlowState: SerializedBytes<FlowState>?,
|
||||
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],
|
||||
* changing the status to [Checkpoint.FlowStatus.PAUSED].
|
||||
@ -85,6 +95,4 @@ interface CheckpointStorage {
|
||||
fun getFlowException(id: StateMachineRunId, throwIfMissing: Boolean = false): Any?
|
||||
|
||||
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.User
|
||||
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
||||
import net.corda.nodeapi.internal.persistence.SchemaInitializationType
|
||||
import net.corda.tools.shell.SSHDConfiguration
|
||||
import java.net.URL
|
||||
import java.nio.file.Path
|
||||
@ -132,8 +131,6 @@ data class NodeConfigurationImpl(
|
||||
fun messagingServerExternal(messagingServerAddress: NetworkHostAndPort?) = messagingServerAddress != null
|
||||
|
||||
fun database(devMode: Boolean) = DatabaseConfig(
|
||||
initialiseSchema = devMode,
|
||||
initialiseAppSchema = if(devMode) SchemaInitializationType.UPDATE else SchemaInitializationType.VALIDATE,
|
||||
exportHibernateJMXStatistics = devMode
|
||||
)
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ import net.corda.common.validation.internal.Validated.Companion.invalid
|
||||
import net.corda.common.validation.internal.Validated.Companion.valid
|
||||
import net.corda.core.context.AuthServiceId
|
||||
import net.corda.core.internal.notary.NotaryServiceFlow
|
||||
import net.corda.node.internal.ConfigurationException
|
||||
import net.corda.node.services.config.AuthDataSourceType
|
||||
import net.corda.node.services.config.CertChainPolicyConfig
|
||||
import net.corda.node.services.config.CertChainPolicyType
|
||||
@ -44,7 +45,6 @@ import net.corda.nodeapi.BrokerRpcSslOptions
|
||||
import net.corda.nodeapi.internal.config.User
|
||||
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
||||
import net.corda.nodeapi.internal.persistence.TransactionIsolationLevel
|
||||
import net.corda.nodeapi.internal.persistence.SchemaInitializationType
|
||||
import net.corda.notary.experimental.bftsmart.BFTSmartConfig
|
||||
import net.corda.notary.experimental.raft.RaftConfig
|
||||
import net.corda.tools.shell.SSHDConfiguration
|
||||
@ -267,16 +267,32 @@ internal object SSHDConfigurationSpec : Configuration.Specification<SSHDConfigur
|
||||
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") {
|
||||
private val initialiseSchema by boolean().optional().withDefaultValue(DatabaseConfig.Defaults.initialiseSchema)
|
||||
private val initialiseAppSchema by enum(SchemaInitializationType::class).optional().withDefaultValue(DatabaseConfig.Defaults.initialiseAppSchema)
|
||||
private val transactionIsolationLevel by enum(TransactionIsolationLevel::class).optional().withDefaultValue(DatabaseConfig.Defaults.transactionIsolationLevel)
|
||||
private val initialiseSchema by boolean().optional()
|
||||
private val initialiseAppSchema by enum(SchemaInitializationType::class).optional()
|
||||
private val transactionIsolationLevel by enum(TransactionIsolationLevel::class).optional()
|
||||
private val exportHibernateJMXStatistics by boolean().optional().withDefaultValue(DatabaseConfig.Defaults.exportHibernateJMXStatistics)
|
||||
private val mappedSchemaCacheSize by long().optional().withDefaultValue(DatabaseConfig.Defaults.mappedSchemaCacheSize)
|
||||
|
||||
override fun parseValid(configuration: Config, options: Configuration.Options): Valid<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)
|
||||
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.hash
|
||||
import net.corda.core.internal.toSet
|
||||
import net.corda.core.node.NotaryInfo
|
||||
import net.corda.core.node.services.UnknownAnonymousPartyException
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
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.node.services.api.IdentityServiceInternal
|
||||
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.WritablePublicKeyToOwningIdentityCache
|
||||
import net.corda.node.utilities.AppendOnlyPersistentMap
|
||||
@ -53,7 +55,8 @@ import kotlin.streams.toList
|
||||
* cached for efficient lookup.
|
||||
*/
|
||||
@ThreadSafe
|
||||
class PersistentIdentityService(cacheFactory: NamedCacheFactory) : SingletonSerializeAsToken(), IdentityServiceInternal {
|
||||
@Suppress("TooManyFunctions")
|
||||
class PersistentIdentityService(cacheFactory: NamedCacheFactory) : SingletonSerializeAsToken(), IdentityServiceInternal, NotaryUpdateListener {
|
||||
|
||||
companion object {
|
||||
private val log = contextLogger()
|
||||
@ -197,7 +200,8 @@ class PersistentIdentityService(cacheFactory: NamedCacheFactory) : SingletonSeri
|
||||
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. */
|
||||
private val notaryIdentityCache = HashSet<Party>()
|
||||
@Volatile
|
||||
private var notaryIdentityCache = HashSet<Party>()
|
||||
|
||||
// CordaPersistence is not a c'tor parameter to work around the cyclic dependency
|
||||
lateinit var database: CordaPersistence
|
||||
@ -453,4 +457,8 @@ class PersistentIdentityService(cacheFactory: NamedCacheFactory) : SingletonSeri
|
||||
keys
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewNotaryList(notaries: List<NotaryInfo>) {
|
||||
notaryIdentityCache = HashSet(notaries.map { it.identity })
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
package net.corda.node.services.network
|
||||
|
||||
import com.google.common.util.concurrent.MoreExecutors
|
||||
import net.corda.cliutils.ExitCodes
|
||||
import net.corda.core.CordaRuntimeException
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.SignedData
|
||||
@ -62,7 +63,7 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal,
|
||||
private val baseDirectory: Path,
|
||||
private val extraNetworkMapKeys: List<UUID>,
|
||||
private val networkParametersStorage: NetworkParametersStorage
|
||||
) : AutoCloseable {
|
||||
) : AutoCloseable, NetworkParameterUpdateListener {
|
||||
companion object {
|
||||
private val logger = contextLogger()
|
||||
private val defaultRetryInterval = 1.minutes
|
||||
@ -77,12 +78,15 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal,
|
||||
private val fileWatcherSubscription = AtomicReference<Subscription?>()
|
||||
private var autoAcceptNetworkParameters: Boolean = true
|
||||
private lateinit var trustRoot: X509Certificate
|
||||
@Volatile
|
||||
private lateinit var currentParametersHash: SecureHash
|
||||
private lateinit var ourNodeInfo: SignedNodeInfo
|
||||
private lateinit var ourNodeInfoHash: SecureHash
|
||||
|
||||
private lateinit var networkParameters: NetworkParameters
|
||||
private lateinit var keyManagementService: KeyManagementService
|
||||
private lateinit var excludedAutoAcceptNetworkParameters: Set<String>
|
||||
private var networkParametersHotloader: NetworkParametersHotloader? = null
|
||||
|
||||
override fun close() {
|
||||
fileWatcherSubscription.updateAndGet { subscription ->
|
||||
@ -95,13 +99,15 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal,
|
||||
}
|
||||
MoreExecutors.shutdownAndAwaitTermination(networkMapPoller, 50, TimeUnit.SECONDS)
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
fun start(trustRoot: X509Certificate,
|
||||
currentParametersHash: SecureHash,
|
||||
ourNodeInfo: SignedNodeInfo,
|
||||
networkParameters: NetworkParameters,
|
||||
keyManagementService: KeyManagementService,
|
||||
networkParameterAcceptanceSettings: NetworkParameterAcceptanceSettings) {
|
||||
networkParameterAcceptanceSettings: NetworkParameterAcceptanceSettings,
|
||||
networkParametersHotloader: NetworkParametersHotloader?
|
||||
) {
|
||||
fileWatcherSubscription.updateAndGet { subscription ->
|
||||
require(subscription == null) { "Should not call this method twice" }
|
||||
this.trustRoot = trustRoot
|
||||
@ -112,6 +118,8 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal,
|
||||
this.keyManagementService = keyManagementService
|
||||
this.autoAcceptNetworkParameters = networkParameterAcceptanceSettings.autoAcceptEnabled
|
||||
this.excludedAutoAcceptNetworkParameters = networkParameterAcceptanceSettings.excludedAutoAcceptableParameters
|
||||
this.networkParametersHotloader = networkParametersHotloader
|
||||
|
||||
|
||||
val autoAcceptNetworkParametersNames = autoAcceptablePropertyNames - excludedAutoAcceptNetworkParameters
|
||||
if (autoAcceptNetworkParameters && autoAcceptNetworkParametersNames.isNotEmpty()) {
|
||||
@ -180,7 +188,7 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal,
|
||||
val additionalHashes = getPrivateNetworkNodeHashes(version)
|
||||
val allHashesFromNetworkMap = (globalNetworkMap.nodeInfoHashes + additionalHashes).toSet()
|
||||
if (currentParametersHash != globalNetworkMap.networkParameterHash) {
|
||||
exitOnParametersMismatch(globalNetworkMap)
|
||||
hotloadOrExitOnParametersMismatch(globalNetworkMap)
|
||||
}
|
||||
// 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
|
||||
@ -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 acceptedHash = if (updatesFile.exists()) updatesFile.readObject<SignedNetworkParameters>().raw.hash else null
|
||||
val exitCode = if (acceptedHash == networkMap.networkParameterHash) {
|
||||
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.")
|
||||
0
|
||||
} else {
|
||||
// TODO This needs special handling (node omitted update process or didn't accept new parameters)
|
||||
val newParameterHash = networkMap.networkParameterHash
|
||||
val nodeAcceptedNewParameters = updatesFile.exists() && newParameterHash == updatesFile.readObject<SignedNetworkParameters>().raw.hash
|
||||
|
||||
if (!nodeAcceptedNewParameters) {
|
||||
logger.error(
|
||||
"""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.
|
||||
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) {
|
||||
@ -340,6 +352,10 @@ The node will shutdown now.""")
|
||||
throw OutdatedNetworkParameterHashException(parametersHash, newParametersHash)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewNetworkParameters(networkParameters: NetworkParameters) {
|
||||
this.networkParameters = networkParameters
|
||||
}
|
||||
}
|
||||
|
||||
private val memberPropertyPartition = NetworkParameters::class.declaredMemberProperties.partition { it.isAutoAcceptable() }
|
||||
@ -360,8 +376,8 @@ internal fun NetworkParameters.canAutoAccept(newNetworkParameters: NetworkParame
|
||||
|
||||
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 newPropertyValue = getter?.invoke(newNetworkParameters)
|
||||
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. */
|
||||
@ThreadSafe
|
||||
@Suppress("TooManyFunctions")
|
||||
open class PersistentNetworkMapCache(cacheFactory: NamedCacheFactory,
|
||||
private val database: CordaPersistence,
|
||||
private val identityService: IdentityService) : NetworkMapCacheInternal, SingletonSerializeAsToken() {
|
||||
private val identityService: IdentityService) : NetworkMapCacheInternal, SingletonSerializeAsToken(), NotaryUpdateListener {
|
||||
|
||||
companion object {
|
||||
private val logger = contextLogger()
|
||||
@ -53,6 +54,7 @@ open class PersistentNetworkMapCache(cacheFactory: NamedCacheFactory,
|
||||
|
||||
override val nodeReady: OpenFuture<Void?> = openFuture()
|
||||
|
||||
@Volatile
|
||||
private lateinit var notaries: List<NotaryInfo>
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewNotaryList(notaries: List<NotaryInfo>) {
|
||||
this.notaries = notaries
|
||||
}
|
||||
}
|
||||
|
@ -599,6 +599,11 @@ class DBCheckpointStorage(
|
||||
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 {
|
||||
val context = checkpoint.checkpointState.invocationContext
|
||||
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,
|
||||
// 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.
|
||||
@ -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> {
|
||||
return WeightBasedAppendOnlyPersistentMap<SecureHash, TxCacheValue, DBTransaction, String>(
|
||||
cacheFactory = cacheFactory,
|
||||
@ -221,12 +223,22 @@ class DBTransactionStorage(private val database: CordaPersistence, cacheFactory:
|
||||
}
|
||||
|
||||
override fun trackTransaction(id: SecureHash): CordaFuture<SignedTransaction> {
|
||||
val (transaction, warning) = trackTransactionInternal(id)
|
||||
warning?.also { log.warn(it) }
|
||||
return transaction
|
||||
}
|
||||
|
||||
if (contextTransactionOrNull != null) {
|
||||
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 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 trackTransactionWithNoWarning(id)
|
||||
return Pair(trackTransactionWithNoWarning(id), warning)
|
||||
}
|
||||
|
||||
override fun trackTransactionWithNoWarning(id: SecureHash): CordaFuture<SignedTransaction> {
|
||||
|
@ -62,13 +62,12 @@ class NodeSchemaService(private val extraSchemas: Set<MappedSchema> = emptySet()
|
||||
NodeInfoSchemaV1,
|
||||
NodeCoreV1)
|
||||
|
||||
fun 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" ||
|
||||
val internalSchemas = requiredSchemas + extraSchemas.filter { schema ->
|
||||
schema::class.qualifiedName?.startsWith("net.corda.notary.") ?: false
|
||||
}
|
||||
|
||||
val appSchemas = extraSchemas - internalSchemas
|
||||
|
||||
override val schemas: Set<MappedSchema> = requiredSchemas + extraSchemas
|
||||
|
||||
// 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()
|
||||
|
||||
/**
|
||||
* 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]
|
||||
* the flow could have persisted its database result or exception.
|
||||
@ -108,6 +113,11 @@ sealed class Action {
|
||||
val lastState: StateMachineState
|
||||
) : Action()
|
||||
|
||||
/**
|
||||
* Move the flow corresponding to [flowId] to paused.
|
||||
*/
|
||||
data class MoveFlowToPaused(val currentState: StateMachineState) : Action()
|
||||
|
||||
/**
|
||||
* Schedule [event] to self.
|
||||
*/
|
||||
|
@ -67,6 +67,8 @@ internal class ActionExecutorImpl(
|
||||
is Action.RetryFlowFromSafePoint -> executeRetryFlowFromSafePoint(action)
|
||||
is Action.ScheduleFlowTimeout -> scheduleFlowTimeout(action)
|
||||
is Action.CancelFlowTimeout -> cancelFlowTimeout(action)
|
||||
is Action.MoveFlowToPaused -> executeMoveFlowToPaused(action)
|
||||
is Action.UpdateFlowStatus -> executeUpdateFlowStatus(action)
|
||||
}
|
||||
}
|
||||
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
|
||||
private fun executePersistDeduplicationIds(action: Action.PersistDeduplicationFacts) {
|
||||
for (handle in action.deduplicationHandlers) {
|
||||
@ -191,6 +198,11 @@ internal class ActionExecutorImpl(
|
||||
stateMachineManager.removeFlow(action.flowId, action.removalReason, action.lastState)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun executeMoveFlowToPaused(action: Action.MoveFlowToPaused) {
|
||||
stateMachineManager.moveFlowToPaused(action.currentState)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
@Throws(SQLException::class)
|
||||
private fun executeCreateTransaction() {
|
||||
|
@ -139,7 +139,7 @@ sealed class 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.
|
||||
*
|
||||
@ -179,6 +179,20 @@ sealed class Event {
|
||||
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,
|
||||
* 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.ServiceHubInternal
|
||||
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.utilities.isEnabledTimedFlow
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
import org.apache.activemq.artemis.utils.ReusableLatch
|
||||
import java.security.SecureRandom
|
||||
import java.util.concurrent.Semaphore
|
||||
|
||||
class Flow<A>(val fiber: FlowStateMachineImpl<A>, val resultFuture: OpenFuture<Any?>)
|
||||
|
||||
class NonResidentFlow(val runId: StateMachineRunId, val checkpoint: Checkpoint) {
|
||||
val resultFuture: OpenFuture<Any?> = openFuture()
|
||||
data class NonResidentFlow(
|
||||
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: Event.DeliverSessionMessage) {
|
||||
externalEvents.add(message)
|
||||
fun addExternalEvent(message: ExternalEvent) {
|
||||
events.add(message)
|
||||
}
|
||||
}
|
||||
|
||||
@ -67,31 +70,49 @@ class FlowCreator(
|
||||
}
|
||||
else -> nonResidentFlow.checkpoint
|
||||
}
|
||||
return createFlowFromCheckpoint(nonResidentFlow.runId, checkpoint, nonResidentFlow.resultFuture)
|
||||
return createFlowFromCheckpoint(nonResidentFlow.runId, checkpoint, resultFuture = nonResidentFlow.resultFuture)
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
fun createFlowFromCheckpoint(
|
||||
runId: StateMachineRunId,
|
||||
oldCheckpoint: Checkpoint,
|
||||
reloadCheckpointAfterSuspendCount: Int? = null,
|
||||
lock: Semaphore = Semaphore(1),
|
||||
resultFuture: OpenFuture<Any?> = openFuture(),
|
||||
reloadCheckpointAfterSuspendCount: Int? = null
|
||||
firstRestore: Boolean = true
|
||||
): Flow<*>? {
|
||||
val checkpoint = oldCheckpoint.copy(status = Checkpoint.FlowStatus.RUNNABLE)
|
||||
val fiber = checkpoint.getFiberFromCheckpoint(runId) ?: return null
|
||||
val fiber = oldCheckpoint.getFiberFromCheckpoint(runId, firstRestore)
|
||||
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
|
||||
verifyFlowLogicIsSuspendable(fiber.logic)
|
||||
val state = createStateMachineState(
|
||||
fiber.transientValues = createTransientValues(runId, resultFuture)
|
||||
fiber.transientState = createStateMachineState(
|
||||
checkpoint = checkpoint,
|
||||
fiber = fiber,
|
||||
anyCheckpointPersisted = true,
|
||||
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)
|
||||
}
|
||||
|
||||
private fun updateCompatibleInDb(runId: StateMachineRunId, compatible: Boolean) {
|
||||
database.transaction {
|
||||
checkpointStorage.updateCompatible(runId, compatible)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
fun <A> createFlowFromLogic(
|
||||
flowId: StateMachineRunId,
|
||||
@ -127,6 +148,7 @@ class FlowCreator(
|
||||
fiber = flowStateMachineImpl,
|
||||
anyCheckpointPersisted = existingCheckpoint != null,
|
||||
reloadCheckpointAfterSuspendCount = if (reloadCheckpointAfterSuspend) 0 else null,
|
||||
lock = Semaphore(1),
|
||||
deduplicationHandler = deduplicationHandler,
|
||||
senderUUID = senderUUID
|
||||
)
|
||||
@ -134,36 +156,45 @@ class FlowCreator(
|
||||
return Flow(flowStateMachineImpl, resultFuture)
|
||||
}
|
||||
|
||||
private fun Checkpoint.getFiberFromCheckpoint(runId: StateMachineRunId): FlowStateMachineImpl<*>? {
|
||||
return when (this.flowState) {
|
||||
is FlowState.Unstarted -> {
|
||||
val logic = tryCheckpointDeserialize(this.flowState.frozenFlowLogic, runId) ?: return null
|
||||
FlowStateMachineImpl(runId, logic, scheduler)
|
||||
}
|
||||
is FlowState.Started -> tryCheckpointDeserialize(this.flowState.frozenFiber, runId) ?: return null
|
||||
// Places calling this function is rely on it to return null if the flow cannot be created from the checkpoint.
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
private inline fun <reified T : Any> tryCheckpointDeserialize(bytes: SerializedBytes<T>, flowId: StateMachineRunId): T? {
|
||||
return try {
|
||||
bytes.checkpointDeserialize(context = checkpointSerializationContext)
|
||||
private fun Checkpoint.getFiberFromCheckpoint(runId: StateMachineRunId, firstRestore: Boolean): FlowStateMachineImpl<*>? {
|
||||
try {
|
||||
return when(flowState) {
|
||||
is FlowState.Unstarted -> {
|
||||
val logic = deserializeFlowState(flowState.frozenFlowLogic)
|
||||
FlowStateMachineImpl(runId, logic, scheduler)
|
||||
}
|
||||
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.
|
||||
else -> return null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (reloadCheckpointAfterSuspend && currentStateMachine() != null) {
|
||||
if (reloadCheckpointAfterSuspend && FlowStateMachineImpl.currentStateMachine() != null) {
|
||||
logger.error(
|
||||
"Unable to deserialize checkpoint for flow $flowId. [reloadCheckpointAfterSuspend] is turned on, throwing exception",
|
||||
e
|
||||
"Unable to deserialize checkpoint for flow $runId. [reloadCheckpointAfterSuspend] is turned on, throwing exception",
|
||||
e
|
||||
)
|
||||
throw ReloadFlowFromCheckpointException(e)
|
||||
} else {
|
||||
logger.error("Unable to deserialize checkpoint for flow $flowId. Something is very wrong and this flow will be ignored.", e)
|
||||
null
|
||||
logSerializationError(firstRestore, runId, e)
|
||||
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?>) {
|
||||
// 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.
|
||||
@ -198,6 +229,7 @@ class FlowCreator(
|
||||
fiber: FlowStateMachineImpl<*>,
|
||||
anyCheckpointPersisted: Boolean,
|
||||
reloadCheckpointAfterSuspendCount: Int?,
|
||||
lock: Semaphore,
|
||||
deduplicationHandler: DeduplicationHandler? = null,
|
||||
senderUUID: String? = null
|
||||
): StateMachineState {
|
||||
@ -213,7 +245,8 @@ class FlowCreator(
|
||||
isKilled = false,
|
||||
flowLogic = fiber.logic,
|
||||
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 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.
|
||||
* Try to avoid using this directly, instead use [processEventsUntilFlowIsResumed] or [processEventImmediately]
|
||||
@ -164,20 +174,23 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
|
||||
*/
|
||||
@Suspendable
|
||||
private fun processEvent(transitionExecutor: TransitionExecutor, event: Event): FlowContinuation {
|
||||
setLoggingContext()
|
||||
val stateMachine = transientValues.stateMachine
|
||||
val oldState = transientState
|
||||
val actionExecutor = transientValues.actionExecutor
|
||||
val transition = stateMachine.transition(event, oldState)
|
||||
val (continuation, newState) = transitionExecutor.executeTransition(this, oldState, event, transition, actionExecutor)
|
||||
// Ensure that the next state that is being written to the transient state maintains the [isKilled] flag
|
||||
// This condition can be met if a flow is killed during [TransitionExecutor.executeTransition]
|
||||
if (oldState.isKilled && !newState.isKilled) {
|
||||
newState.isKilled = true
|
||||
return withFlowLock {
|
||||
setLoggingContext()
|
||||
val stateMachine = transientValues.stateMachine
|
||||
val oldState = transientState
|
||||
val actionExecutor = transientValues.actionExecutor
|
||||
val transition = stateMachine.transition(event, oldState)
|
||||
val (continuation, newState) = transitionExecutor.executeTransition(
|
||||
this,
|
||||
oldState,
|
||||
event,
|
||||
transition,
|
||||
actionExecutor
|
||||
)
|
||||
transientState = newState
|
||||
setLoggingContext()
|
||||
continuation
|
||||
}
|
||||
transientState = newState
|
||||
setLoggingContext()
|
||||
return continuation
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -3,6 +3,7 @@ package net.corda.node.services.statemachine
|
||||
import co.paralleluniverse.fibers.Fiber
|
||||
import co.paralleluniverse.fibers.FiberExecutorScheduler
|
||||
import co.paralleluniverse.fibers.instrument.JavaAgent
|
||||
import co.paralleluniverse.strands.channels.Channel
|
||||
import com.codahale.metrics.Gauge
|
||||
import com.google.common.util.concurrent.ThreadFactoryBuilder
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
@ -58,7 +59,6 @@ import javax.annotation.concurrent.ThreadSafe
|
||||
import kotlin.collections.component1
|
||||
import kotlin.collections.component2
|
||||
import kotlin.collections.set
|
||||
import kotlin.streams.toList
|
||||
|
||||
/**
|
||||
* The StateMachineManagerImpl will always invoke the flow fibers on the given [AffinityExecutor], regardless of which
|
||||
@ -77,6 +77,14 @@ internal class SingleThreadedStateMachineManager(
|
||||
companion object {
|
||||
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
|
||||
var beforeClientIDCheck: (() -> Unit)? = null
|
||||
@VisibleForTesting
|
||||
@ -102,7 +110,7 @@ internal class SingleThreadedStateMachineManager(
|
||||
private val flowTimeoutScheduler = FlowTimeoutScheduler(innerState, scheduledFutureExecutor, serviceHub)
|
||||
private val ourSenderUUID = serviceHub.networkService.ourSenderUUID
|
||||
|
||||
private var checkpointSerializationContext: CheckpointSerializationContext? = null
|
||||
private lateinit var checkpointSerializationContext: CheckpointSerializationContext
|
||||
private lateinit var flowCreator: FlowCreator
|
||||
|
||||
override val flowHospital: StaffedFlowHospital = makeFlowHospital()
|
||||
@ -115,6 +123,26 @@ internal class SingleThreadedStateMachineManager(
|
||||
private val totalStartedFlows = metrics.counter("Flows.Started")
|
||||
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
|
||||
* which may change across restarts.
|
||||
@ -153,12 +181,11 @@ internal class SingleThreadedStateMachineManager(
|
||||
flowTimeoutScheduler::resetCustomTimeout
|
||||
)
|
||||
|
||||
val fibers = restoreFlowsFromCheckpoints()
|
||||
val (fibers, pausedFlows) = restoreFlowsFromCheckpoints()
|
||||
metrics.register("Flows.InFlight", Gauge<Int> { innerState.flows.size })
|
||||
|
||||
setFlowDefaultUncaughtExceptionHandler()
|
||||
|
||||
val pausedFlows = restoreNonResidentFlowsFromPausedCheckpoints()
|
||||
innerState.withLock {
|
||||
this.pausedFlows.putAll(pausedFlows)
|
||||
for ((id, flow) in pausedFlows) {
|
||||
@ -322,9 +349,9 @@ internal class SingleThreadedStateMachineManager(
|
||||
}
|
||||
|
||||
override fun killFlow(id: StateMachineRunId): Boolean {
|
||||
val killFlowResult = innerState.withLock {
|
||||
val flow = flows[id]
|
||||
if (flow != null) {
|
||||
val flow = innerState.withLock { flows[id] }
|
||||
val killFlowResult = if (flow != null) {
|
||||
flow.withFlowLock(VALID_KILL_FLOW_STATUSES) {
|
||||
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 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)
|
||||
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()
|
||||
|
||||
val state = flow.fiber.transientState
|
||||
state.isKilled = true
|
||||
flow.fiber.scheduleEvent(Event.DoRemainingWork)
|
||||
flow.fiber.transientState = flow.fiber.transientState.copy(isKilled = true)
|
||||
scheduleEvent(Event.DoRemainingWork)
|
||||
true
|
||||
} 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.
|
||||
database.transaction { checkpointStorage.removeCheckpoint(id, mayHavePersistentResults = true) }
|
||||
}
|
||||
}
|
||||
return if (killFlowResult) {
|
||||
true
|
||||
} else {
|
||||
flowHospital.dropSessionInit(id)
|
||||
}
|
||||
|
||||
return killFlowResult || flowHospital.dropSessionInit(id)
|
||||
}
|
||||
|
||||
private fun markAllFlowsAsPaused() {
|
||||
@ -425,38 +447,39 @@ internal class SingleThreadedStateMachineManager(
|
||||
liveFibers.countUp()
|
||||
}
|
||||
|
||||
private fun restoreFlowsFromCheckpoints(): List<Flow<*>> {
|
||||
return checkpointStorage.getCheckpointsToRun().use {
|
||||
it.mapNotNull { (id, serializedCheckpoint) ->
|
||||
// If a flow is added before start() then don't attempt to restore it
|
||||
innerState.withLock { if (id in flows) return@mapNotNull null }
|
||||
val checkpoint = tryDeserializeCheckpoint(serializedCheckpoint, id)?.also {
|
||||
if (it.status == Checkpoint.FlowStatus.HOSPITALIZED) {
|
||||
checkpointStorage.updateStatus(id, Checkpoint.FlowStatus.RUNNABLE)
|
||||
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.")
|
||||
return@mapNotNull null
|
||||
}
|
||||
private fun restoreFlowsFromCheckpoints(): Pair<MutableMap<StateMachineRunId, Flow<*>>, MutableMap<StateMachineRunId, NonResidentFlow>> {
|
||||
val flows = mutableMapOf<StateMachineRunId, Flow<*>>()
|
||||
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
|
||||
innerState.withLock { if (id in flows) return@Checkpoints }
|
||||
val checkpoint = tryDeserializeCheckpoint(serializedCheckpoint, id)?.also {
|
||||
if (it.status == Checkpoint.FlowStatus.HOSPITALIZED) {
|
||||
checkpointStorage.updateStatus(id, Checkpoint.FlowStatus.RUNNABLE)
|
||||
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.")
|
||||
return@Checkpoints
|
||||
}
|
||||
} ?: return@mapNotNull null
|
||||
flowCreator.createFlowFromCheckpoint(id, checkpoint)
|
||||
}.toList()
|
||||
}
|
||||
} ?: return@Checkpoints
|
||||
val flow = flowCreator.createFlowFromCheckpoint(id, checkpoint)
|
||||
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> {
|
||||
return checkpointStorage.getPausedCheckpoints().use {
|
||||
it.mapNotNull { (id, serializedCheckpoint) ->
|
||||
// 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)
|
||||
private fun resumeRestoredFlows(flows: Map<StateMachineRunId, Flow<*>>) {
|
||||
for ((id, flow) in flows.entries) {
|
||||
addAndStartFlow(id, flow)
|
||||
}
|
||||
}
|
||||
|
||||
@ -492,8 +515,13 @@ internal class SingleThreadedStateMachineManager(
|
||||
} ?: return
|
||||
|
||||
// Resurrect flow
|
||||
flowCreator.createFlowFromCheckpoint(flowId, checkpoint, reloadCheckpointAfterSuspendCount = currentState.reloadCheckpointAfterSuspendCount)
|
||||
?: return
|
||||
flowCreator.createFlowFromCheckpoint(
|
||||
flowId,
|
||||
checkpoint,
|
||||
currentState.reloadCheckpointAfterSuspendCount,
|
||||
currentState.lock,
|
||||
firstRestore = false
|
||||
) ?: return
|
||||
} else {
|
||||
// Just flow initiation message
|
||||
null
|
||||
@ -510,17 +538,56 @@ internal class SingleThreadedStateMachineManager(
|
||||
injectOldProgressTracker(currentState.flowLogic.progressTracker, flow.fiber.logic)
|
||||
addAndStartFlow(flowId, flow)
|
||||
}
|
||||
// Deliver all the external events from the old flow instance.
|
||||
val unprocessedExternalEvents = mutableListOf<ExternalEvent>()
|
||||
do {
|
||||
val event = oldFlowLeftOver.tryReceive()
|
||||
if (event is Event.GeneratedByExternalEvent) {
|
||||
unprocessedExternalEvents += event.deduplicationHandler.externalCause
|
||||
}
|
||||
} while (event != null)
|
||||
val externalEvents = currentState.pendingDeduplicationHandlers.map { it.externalCause } + unprocessedExternalEvents
|
||||
for (externalEvent in externalEvents) {
|
||||
deliverExternalEvent(externalEvent)
|
||||
extractAndScheduleEventsForRetry(oldFlowLeftOver, currentState)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
val event = currentEventQueue.tryReceive()
|
||||
if (event is Event.GeneratedByExternalEvent) {
|
||||
pausedFlow.events.add(event.deduplicationHandler.externalCause)
|
||||
}
|
||||
} while (event != null)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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)
|
||||
if (sender != null) {
|
||||
when (sessionMessage) {
|
||||
is ExistingSessionMessage -> onExistingSessionMessage(sessionMessage, event.deduplicationHandler, sender)
|
||||
is ExistingSessionMessage -> onExistingSessionMessage(sessionMessage, sender, event)
|
||||
is InitialSessionMessage -> onSessionInit(sessionMessage, sender, event)
|
||||
}
|
||||
} 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 {
|
||||
val deduplicationHandler = externalEvent.deduplicationHandler
|
||||
val recipientId = sessionMessage.recipientSessionId
|
||||
val flowId = sessionToFlow[recipientId]
|
||||
if (flowId == null) {
|
||||
@ -589,7 +661,7 @@ internal class SingleThreadedStateMachineManager(
|
||||
innerState.withLock {
|
||||
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.
|
||||
?: pausedFlows[flowId]?.run { addExternalEvent(event) }
|
||||
?: pausedFlows[flowId]?.run { addExternalEvent(externalEvent) }
|
||||
?: logger.info("Cannot find fiber corresponding to flow ID $flowId")
|
||||
}
|
||||
}
|
||||
@ -699,7 +771,16 @@ internal class SingleThreadedStateMachineManager(
|
||||
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>()
|
||||
innerState.withLock {
|
||||
startedFutures[flowId] = startedFuture
|
||||
@ -717,9 +798,29 @@ internal class SingleThreadedStateMachineManager(
|
||||
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? {
|
||||
return try {
|
||||
serializedCheckpoint.deserialize(checkpointSerializationContext!!)
|
||||
serializedCheckpoint.deserialize(checkpointSerializationContext)
|
||||
} catch (e: Exception) {
|
||||
if (reloadCheckpointAfterSuspend && currentStateMachine() != null) {
|
||||
logger.error(
|
||||
|
@ -104,6 +104,16 @@ class StaffedFlowHospital(private val flowMessaging: FlowMessaging,
|
||||
*/
|
||||
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 {
|
||||
/**
|
||||
* 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 removeSessionBindings(sessionIds: Set<SessionId>)
|
||||
fun removeFlow(flowId: StateMachineRunId, removalReason: FlowRemovalReason, lastState: StateMachineState)
|
||||
fun moveFlowToPaused(currentState: StateMachineState)
|
||||
fun retryFlowFromSafePoint(currentState: StateMachineState)
|
||||
fun scheduleFlowTimeout(flowId: StateMachineRunId)
|
||||
fun cancelFlowTimeout(flowId: StateMachineRunId)
|
||||
|
@ -25,6 +25,7 @@ import net.corda.node.services.messaging.DeduplicationHandler
|
||||
import java.lang.IllegalStateException
|
||||
import java.time.Instant
|
||||
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
|
||||
@ -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
|
||||
* 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
|
||||
* 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
|
||||
* is killed during the middle of a state transition.
|
||||
* what event it is set to process next.
|
||||
* @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 evaluate persistent datastructure libraries to replace the inefficient copying we currently do.
|
||||
@ -60,10 +64,10 @@ data class StateMachineState(
|
||||
val isAnyCheckpointPersisted: Boolean,
|
||||
val isStartIdempotent: Boolean,
|
||||
val isRemoved: Boolean,
|
||||
@Volatile
|
||||
var isKilled: Boolean,
|
||||
val isKilled: Boolean,
|
||||
val senderUUID: String?,
|
||||
val reloadCheckpointAfterSuspendCount: Int?
|
||||
val reloadCheckpointAfterSuspendCount: Int?,
|
||||
val lock: Semaphore
|
||||
) : KryoSerializable {
|
||||
override fun write(kryo: Kryo?, output: Output?) {
|
||||
throw IllegalStateException("${StateMachineState::class.qualifiedName} should never be serialized")
|
||||
|
@ -41,47 +41,18 @@ class StartedFlowTransition(
|
||||
continuation = FlowContinuation.Throw(errorsToThrow[0])
|
||||
)
|
||||
}
|
||||
val sessionsToBeTerminated = findSessionsToBeTerminated(startingState)
|
||||
// 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.Receive -> receiveTransition(flowIORequest)
|
||||
is FlowIORequest.SendAndReceive -> sendAndReceiveTransition(flowIORequest)
|
||||
is FlowIORequest.CloseSessions -> closeSessionTransition(flowIORequest)
|
||||
is FlowIORequest.WaitForLedgerCommit -> waitForLedgerCommitTransition(flowIORequest)
|
||||
is FlowIORequest.Sleep -> sleepTransition(flowIORequest)
|
||||
is FlowIORequest.GetFlowInfo -> getFlowInfoTransition(flowIORequest)
|
||||
is FlowIORequest.WaitForSessionConfirmations -> waitForSessionConfirmationsTransition()
|
||||
is FlowIORequest.ExecuteAsyncOperation<*> -> executeAsyncOperation(flowIORequest)
|
||||
FlowIORequest.ForceCheckpoint -> executeForceCheckpoint()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
return when (flowIORequest) {
|
||||
is FlowIORequest.Send -> sendTransition(flowIORequest)
|
||||
is FlowIORequest.Receive -> receiveTransition(flowIORequest)
|
||||
is FlowIORequest.SendAndReceive -> sendAndReceiveTransition(flowIORequest)
|
||||
is FlowIORequest.CloseSessions -> closeSessionTransition(flowIORequest)
|
||||
is FlowIORequest.WaitForLedgerCommit -> waitForLedgerCommitTransition(flowIORequest)
|
||||
is FlowIORequest.Sleep -> sleepTransition(flowIORequest)
|
||||
is FlowIORequest.GetFlowInfo -> getFlowInfoTransition(flowIORequest)
|
||||
is FlowIORequest.WaitForSessionConfirmations -> waitForSessionConfirmationsTransition()
|
||||
is FlowIORequest.ExecuteAsyncOperation<*> -> executeAsyncOperation(flowIORequest)
|
||||
FlowIORequest.ForceCheckpoint -> executeForceCheckpoint()
|
||||
}.let { scheduleTerminateSessionsIfRequired(it) }
|
||||
}
|
||||
|
||||
private fun waitForSessionConfirmationsTransition(): TransitionResult {
|
||||
@ -158,6 +129,7 @@ class StartedFlowTransition(
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
private fun sendAndReceiveTransition(flowIORequest: FlowIORequest.SendAndReceive): TransitionResult {
|
||||
val sessionIdToMessage = LinkedHashMap<SessionId, SerializedBytes<Any>>()
|
||||
val sessionIdToSession = LinkedHashMap<SessionId, FlowSessionImpl>()
|
||||
@ -171,18 +143,23 @@ class StartedFlowTransition(
|
||||
if (isErrored()) {
|
||||
FlowContinuation.ProcessEvents
|
||||
} else {
|
||||
val receivedMap = receiveFromSessionsTransition(sessionIdToSession)
|
||||
if (receivedMap == null) {
|
||||
// We don't yet have the messages, change the suspension to be on Receive
|
||||
val newIoRequest = FlowIORequest.Receive(flowIORequest.sessionToMessage.keys.toNonEmptySet())
|
||||
currentState = currentState.copy(
|
||||
try {
|
||||
val receivedMap = receiveFromSessionsTransition(sessionIdToSession)
|
||||
if (receivedMap == null) {
|
||||
// We don't yet have the messages, change the suspension to be on Receive
|
||||
val newIoRequest = FlowIORequest.Receive(flowIORequest.sessionToMessage.keys.toNonEmptySet())
|
||||
currentState = currentState.copy(
|
||||
checkpoint = currentState.checkpoint.copy(
|
||||
flowState = FlowState.Started(newIoRequest, started.frozenFiber)
|
||||
flowState = FlowState.Started(newIoRequest, started.frozenFiber)
|
||||
)
|
||||
)
|
||||
FlowContinuation.ProcessEvents
|
||||
} else {
|
||||
resumeFlowLogic(receivedMap)
|
||||
)
|
||||
FlowContinuation.ProcessEvents
|
||||
} else {
|
||||
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 {
|
||||
return builder {
|
||||
val sessionIdToSession = LinkedHashMap<SessionId, FlowSessionImpl>()
|
||||
@ -224,11 +202,16 @@ class StartedFlowTransition(
|
||||
}
|
||||
// send initialises to uninitialised sessions
|
||||
sendInitialSessionMessagesIfNeeded(sessionIdToSession.keys)
|
||||
val receivedMap = receiveFromSessionsTransition(sessionIdToSession)
|
||||
if (receivedMap == null) {
|
||||
FlowContinuation.ProcessEvents
|
||||
} else {
|
||||
resumeFlowLogic(receivedMap)
|
||||
try {
|
||||
val receivedMap = receiveFromSessionsTransition(sessionIdToSession)
|
||||
if (receivedMap == null) {
|
||||
FlowContinuation.ProcessEvents
|
||||
} else {
|
||||
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 newSessionMap: SessionMap
|
||||
)
|
||||
|
||||
@Suppress("ComplexMethod", "NestedBlockDepth")
|
||||
private fun pollSessionMessages(sessions: SessionMap, sessionIds: Set<SessionId>): PollResult? {
|
||||
val newSessionMessages = LinkedHashMap(sessions)
|
||||
val resultMessages = LinkedHashMap<SessionId, SerializedBytes<Any>>()
|
||||
@ -267,7 +252,11 @@ class StartedFlowTransition(
|
||||
} else {
|
||||
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.
|
||||
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 -> {
|
||||
@ -537,4 +526,25 @@ class StartedFlowTransition(
|
||||
private fun executeForceCheckpoint(): TransitionResult {
|
||||
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.OvernightObservation -> overnightObservationTransition()
|
||||
is Event.WakeUpFromSleep -> wakeUpFromSleepTransition()
|
||||
is Event.Pause -> pausedFlowTransition()
|
||||
is Event.TerminateSessions -> terminateSessionsTransition(event)
|
||||
}
|
||||
}
|
||||
|
||||
@ -378,4 +380,32 @@ class TopLevelTransition(
|
||||
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 = []
|
||||
crlCheckSoftFail = true
|
||||
database = {
|
||||
transactionIsolationLevel = "REPEATABLE_READ"
|
||||
exportHibernateJMXStatistics = "false"
|
||||
}
|
||||
dataSourceProperties = {
|
||||
|
@ -6,6 +6,8 @@ import net.corda.core.context.InvocationOrigin
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.flows.FlowLogic
|
||||
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.ServiceHub
|
||||
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.finance.DOLLARS
|
||||
import net.corda.finance.flows.CashIssueFlow
|
||||
import net.corda.finance.schemas.CashSchemaV1
|
||||
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.MockNetworkParameters
|
||||
import net.corda.testing.node.MockServices
|
||||
import net.corda.testing.node.StartedMockNode
|
||||
import net.corda.testing.node.internal.FINANCE_CONTRACTS_CORDAPP
|
||||
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.Before
|
||||
import org.junit.Test
|
||||
@ -100,6 +110,22 @@ class CordaServiceTest {
|
||||
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
|
||||
class DummyServiceFlow : FlowLogic<InvocationContext>() {
|
||||
companion object {
|
||||
|
@ -280,7 +280,7 @@ class NodeConfigurationImplTest {
|
||||
|
||||
@Test(timeout=3_000)
|
||||
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(
|
||||
devMode = false,
|
||||
compatibilityZoneURL = compatibilityZoneURL)
|
||||
|
@ -78,7 +78,6 @@ class NetworkMapUpdaterTest {
|
||||
@Rule
|
||||
@JvmField
|
||||
val testSerialization = SerializationEnvironmentRule(true)
|
||||
|
||||
private val cacheExpiryMs = 1000
|
||||
private val privateNetUUID = UUID.randomUUID()
|
||||
private val fs = Jimfs.newFileSystem(unix())
|
||||
@ -120,12 +119,13 @@ class NetworkMapUpdaterTest {
|
||||
networkParameters: NetworkParameters = server.networkParameters,
|
||||
autoAcceptNetworkParameters: Boolean = true,
|
||||
excludedAutoAcceptNetworkParameters: Set<String> = emptySet()) {
|
||||
|
||||
updater!!.start(DEV_ROOT_CA.certificate,
|
||||
server.networkParameters.serialize().hash,
|
||||
ourNodeInfo,
|
||||
networkParameters,
|
||||
MockKeyManagementService(makeTestIdentityService(), ourKeyPair),
|
||||
NetworkParameterAcceptanceSettings(autoAcceptNetworkParameters, excludedAutoAcceptNetworkParameters))
|
||||
NetworkParameterAcceptanceSettings(autoAcceptNetworkParameters, excludedAutoAcceptNetworkParameters), null)
|
||||
}
|
||||
|
||||
@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()
|
||||
assertEquals(id, extractedId)
|
||||
//We don't extract the result or the flowstate from a paused checkpoint
|
||||
assertEquals(null, extractedCheckpoint.serializedFlowState)
|
||||
assertEquals(null, extractedCheckpoint.result)
|
||||
assertNull(extractedCheckpoint.serializedFlowState)
|
||||
assertNull(extractedCheckpoint.result)
|
||||
|
||||
assertEquals(pausedCheckpoint.status, extractedCheckpoint.status)
|
||||
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)
|
||||
fun `'getFinishedFlowsResultsMetadata' fetches flows results metadata for finished flows only`() {
|
||||
val (_, checkpoint) = newCheckpoint(1)
|
||||
|
@ -9,22 +9,22 @@ import net.corda.core.crypto.SignatureMetadata
|
||||
import net.corda.core.crypto.TransactionSignature
|
||||
import net.corda.core.toFuture
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.node.services.transactions.PersistentUniquenessProvider
|
||||
import net.corda.node.CordaClock
|
||||
import net.corda.node.MutableClock
|
||||
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.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.TestingNamedCacheFactory
|
||||
import net.corda.testing.internal.configureDatabase
|
||||
import net.corda.testing.internal.createWireTransaction
|
||||
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.junit.After
|
||||
import org.junit.Assert
|
||||
@ -32,10 +32,9 @@ import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import rx.plugins.RxJavaHooks
|
||||
import java.io.StringWriter
|
||||
import java.util.concurrent.Semaphore
|
||||
import java.time.Clock
|
||||
import java.time.Instant
|
||||
import java.util.concurrent.Semaphore
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.concurrent.thread
|
||||
import kotlin.test.assertEquals
|
||||
@ -381,47 +380,14 @@ class DBTransactionStorageTests {
|
||||
val signedTransaction = newTransaction()
|
||||
|
||||
// Act
|
||||
val logMessages = collectLogsFrom {
|
||||
database.transaction {
|
||||
val result = transactionStorage.trackTransaction(signedTransaction.id)
|
||||
result.cancel(false)
|
||||
}
|
||||
val warning = database.transaction {
|
||||
val (result, warning) = transactionStorage.trackTransactionInternal(signedTransaction.id)
|
||||
result.cancel(false)
|
||||
warning
|
||||
}
|
||||
|
||||
// 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.")
|
||||
}
|
||||
|
||||
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()
|
||||
assertThat(warning).isEqualTo(DBTransactionStorage.TRANSACTION_ALREADY_IN_PROGRESS_WARNING)
|
||||
}
|
||||
|
||||
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.utilities.contextLogger
|
||||
import net.corda.node.internal.checkOrUpdate
|
||||
import net.corda.node.internal.createCordaPersistence
|
||||
import net.corda.node.internal.startHikariPool
|
||||
import net.corda.node.services.schema.NodeSchemaService
|
||||
import net.corda.node.utilities.AppendOnlyPersistentMap
|
||||
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
||||
import net.corda.nodeapi.internal.persistence.TransactionIsolationLevel
|
||||
import net.corda.nodeapi.internal.persistence.SchemaMigration
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.TestIdentity
|
||||
import net.corda.testing.internal.TestingNamedCacheFactory
|
||||
@ -91,10 +92,13 @@ class DbMapDeadlockTest {
|
||||
|
||||
fun recreateDeadlock(hikariProperties: Properties) {
|
||||
val cacheFactory = TestingNamedCacheFactory()
|
||||
val dbConfig = DatabaseConfig(initialiseSchema = true, transactionIsolationLevel = TransactionIsolationLevel.READ_COMMITTED)
|
||||
val dbConfig = DatabaseConfig()
|
||||
val schemaService = NodeSchemaService(extraSchemas = setOf(LockDbSchemaV2))
|
||||
createCordaPersistence(dbConfig, { null }, { null }, schemaService, hikariProperties, cacheFactory, null).apply {
|
||||
startHikariPool(hikariProperties, dbConfig, schemaService.schemas, ourName = TestIdentity(ALICE_NAME, 70).name)
|
||||
startHikariPool(hikariProperties) { dataSource, haveCheckpoints ->
|
||||
SchemaMigration(dataSource, null, null, TestIdentity(ALICE_NAME, 70).name)
|
||||
.checkOrUpdate(schemaService.schemas, true, haveCheckpoints, false)
|
||||
}
|
||||
}.use { persistence ->
|
||||
|
||||
// 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.Companion.makeTestDataSourceProperties
|
||||
import org.assertj.core.api.Assertions
|
||||
import org.assertj.core.api.Assertions.`in`
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||
import org.hibernate.SessionFactory
|
||||
@ -976,7 +977,7 @@ class HibernateConfigurationTest {
|
||||
doReturn(it.party).whenever(mock).wellKnownPartyFromX500Name(it.name)
|
||||
}
|
||||
}
|
||||
database = configureDatabase(dataSourceProps, DatabaseConfig(initialiseSchema = initialiseSchema), identityService::wellKnownPartyFromX500Name, identityService::wellKnownPartyFromAnonymous, schemaService)
|
||||
database = configureDatabase(dataSourceProps, DatabaseConfig(), identityService::wellKnownPartyFromX500Name, identityService::wellKnownPartyFromAnonymous, schemaService, runMigrationScripts = initialiseSchema, allowHibernateToManageAppSchema = initialiseSchema)
|
||||
return database
|
||||
}
|
||||
|
||||
|
@ -9,6 +9,7 @@ import net.corda.core.flows.FlowSession
|
||||
import net.corda.core.flows.InitiatedBy
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.flows.KilledFlowException
|
||||
import net.corda.core.flows.UnexpectedFlowEndException
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.Party
|
||||
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.persistence.DBTransactionStorage
|
||||
import net.corda.nodeapi.internal.persistence.contextTransaction
|
||||
import net.corda.testing.common.internal.eventually
|
||||
import net.corda.testing.core.TestIdentity
|
||||
import net.corda.testing.node.internal.InternalMockNetwork
|
||||
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.newContext
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.assertj.core.api.Assertions.assertThatExceptionOfType
|
||||
import org.h2.util.Utils
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertTrue
|
||||
@ -38,7 +39,9 @@ import java.sql.SQLException
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
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.assertFailsWith
|
||||
import kotlin.test.assertNotNull
|
||||
@ -58,7 +61,6 @@ class RetryFlowMockTest {
|
||||
RetryFlow.count = 0
|
||||
SendAndRetryFlow.count = 0
|
||||
RetryInsertFlow.count = 0
|
||||
KeepSendingFlow.count.set(0)
|
||||
StaffedFlowHospital.DatabaseEndocrinologist.customConditions.add { t -> t is LimitedRetryCausingError }
|
||||
StaffedFlowHospital.DatabaseEndocrinologist.customConditions.add { t -> t is RetryCausingError }
|
||||
}
|
||||
@ -99,34 +101,40 @@ class RetryFlowMockTest {
|
||||
fun `Restart does not set senderUUID`() {
|
||||
val messagesSent = Collections.synchronizedList(mutableListOf<Message>())
|
||||
val partyB = nodeB.info.legalIdentities.first()
|
||||
val expectedMessagesSent = CountDownLatch(3)
|
||||
nodeA.setMessagingServiceSpy(object : MessagingServiceSpy() {
|
||||
override fun send(message: Message, target: MessageRecipients, sequenceKey: Any) {
|
||||
messagesSent.add(message)
|
||||
expectedMessagesSent.countDown()
|
||||
messagingService.send(message, target)
|
||||
}
|
||||
})
|
||||
val count = 10000 // Lots of iterations so the flow keeps going long enough
|
||||
nodeA.startFlow(KeepSendingFlow(count, partyB))
|
||||
eventually(duration = Duration.ofSeconds(30), waitBetween = Duration.ofMillis(100)) {
|
||||
assertTrue(messagesSent.isNotEmpty())
|
||||
assertNotNull(messagesSent.first().senderUUID)
|
||||
}
|
||||
nodeA.startFlow(KeepSendingFlow(partyB))
|
||||
KeepSendingFlow.lock.acquire()
|
||||
assertTrue(messagesSent.isNotEmpty())
|
||||
assertNotNull(messagesSent.first().senderUUID)
|
||||
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() {
|
||||
override fun send(message: Message, target: MessageRecipients, sequenceKey: Any) {
|
||||
messagesSent.add(message)
|
||||
expectedMessagesSent.countDown()
|
||||
messagingService.send(message, target)
|
||||
}
|
||||
})
|
||||
// Now short circuit the iterations so the flow finishes soon.
|
||||
KeepSendingFlow.count.set(count - 2)
|
||||
eventually(duration = Duration.ofSeconds(30), waitBetween = Duration.ofMillis(100)) {
|
||||
assertTrue(nodeA.smm.allStateMachines.isEmpty())
|
||||
}
|
||||
ReceiveFlow3.lock.release()
|
||||
assertTrue(expectedMessagesSent.await(20, TimeUnit.SECONDS))
|
||||
assertEquals(3, messagesSent.size)
|
||||
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)
|
||||
fun `Retry duplicate insert`() {
|
||||
assertEquals(Unit, nodeA.startFlow(RetryInsertFlow(1)).get())
|
||||
@ -252,32 +260,36 @@ class RetryFlowMockTest {
|
||||
}
|
||||
|
||||
@InitiatingFlow
|
||||
class KeepSendingFlow(private val i: Int, private val other: Party) : FlowLogic<Unit>() {
|
||||
class KeepSendingFlow(private val other: Party) : FlowLogic<Unit>() {
|
||||
|
||||
companion object {
|
||||
val count = AtomicInteger(0)
|
||||
val lock = Semaphore(0)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
val session = initiateFlow(other)
|
||||
session.send(i.toString())
|
||||
do {
|
||||
logger.info("Sending... $count")
|
||||
session.send("Boo")
|
||||
} while (count.getAndIncrement() < i)
|
||||
session.send("boo")
|
||||
lock.release()
|
||||
session.receive<String>()
|
||||
session.send("boo")
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
@InitiatedBy(KeepSendingFlow::class)
|
||||
class ReceiveFlow3(private val other: FlowSession) : FlowLogic<Unit>() {
|
||||
|
||||
companion object {
|
||||
val lock = Semaphore(0)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
var count = other.receive<String>().unwrap { it.toInt() }
|
||||
while (count-- > 0) {
|
||||
val received = other.receive<String>().unwrap { it }
|
||||
logger.info("Received... $received $count")
|
||||
}
|
||||
other.receive<String>()
|
||||
lock.acquire()
|
||||
other.send("hoo")
|
||||
other.receive<String>()
|
||||
}
|
||||
}
|
||||
|
||||
@ -304,4 +316,27 @@ class RetryFlowMockTest {
|
||||
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 = ""
|
||||
}
|
||||
database = {
|
||||
transactionIsolationLevel = "REPEATABLE_READ"
|
||||
exportHibernateJMXStatistics = "false"
|
||||
}
|
||||
p2pAddress = "localhost:2233"
|
||||
|
@ -11,7 +11,6 @@ dataSourceProperties = {
|
||||
dataSource.password = ""
|
||||
}
|
||||
database = {
|
||||
transactionIsolationLevel = "REPEATABLE_READ"
|
||||
exportHibernateJMXStatistics = "false"
|
||||
}
|
||||
p2pAddress = "localhost:2233"
|
||||
|
@ -12,7 +12,6 @@ dataSourceProperties = {
|
||||
dataSource.password = ""
|
||||
}
|
||||
database = {
|
||||
transactionIsolationLevel = "REPEATABLE_READ"
|
||||
exportHibernateJMXStatistics = "false"
|
||||
}
|
||||
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:workflows')
|
||||
runSchemaMigration = true
|
||||
}
|
||||
node {
|
||||
name "O=Notary Node,L=Zurich,C=CH"
|
||||
|
@ -48,6 +48,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar', nodeTask,
|
||||
nodeDefaults {
|
||||
cordapp project(':finance:workflows')
|
||||
cordapp project(':finance:contracts')
|
||||
runSchemaMigration = true
|
||||
}
|
||||
node {
|
||||
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' ]]]
|
||||
cordapp project(':samples:cordapp-configuration:workflows')
|
||||
runSchemaMigration = true
|
||||
}
|
||||
node {
|
||||
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:workflows-irs')
|
||||
runSchemaMigration = true
|
||||
}
|
||||
node {
|
||||
name "O=Notary Node,L=Zurich,C=CH"
|
||||
|
@ -39,7 +39,9 @@ import org.junit.Test
|
||||
import rx.Observable
|
||||
import java.time.Duration
|
||||
import java.time.LocalDate
|
||||
import org.junit.Ignore
|
||||
|
||||
@Ignore
|
||||
class IRSDemoTest {
|
||||
companion object {
|
||||
private val log = contextLogger()
|
||||
@ -51,7 +53,7 @@ class IRSDemoTest {
|
||||
private val maxWaitTime: Duration = 150.seconds
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `runs IRS demo`() {
|
||||
fun `runs IRS demo`() {
|
||||
springDriver(DriverParameters(
|
||||
useTestClock = true,
|
||||
notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, rpcUsers = rpcUsers)),
|
||||
|
@ -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:workflows')
|
||||
runSchemaMigration = true
|
||||
}
|
||||
node {
|
||||
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"]]
|
||||
cordapp project(':samples:notary-demo:contracts')
|
||||
cordapp project(':samples:notary-demo:workflows')
|
||||
runSchemaMigration = true
|
||||
}
|
||||
node {
|
||||
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:flows')
|
||||
rpcUsers = [['username': "default", 'password': "default", 'permissions': [ 'ALL' ]]]
|
||||
runSchemaMigration = true
|
||||
}
|
||||
node {
|
||||
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:contracts')
|
||||
cordapp project(':samples:trader-demo:workflows-trader')
|
||||
runSchemaMigration = true
|
||||
}
|
||||
node {
|
||||
name "O=Notary Node,L=Zurich,C=CH"
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user