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

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

View File

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

View File

@ -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 {

View File

@ -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)

View File

@ -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'

View File

@ -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()

View File

@ -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)
}

View File

@ -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

View File

@ -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
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -618,7 +618,6 @@
<ID>LongParameterList:ArtemisRpcBroker.kt$ArtemisRpcBroker.Companion$(configuration: MutualSslConfiguration, address: NetworkHostAndPort, adminAddress: NetworkHostAndPort, securityManager: RPCSecurityManager, maxMessageSize: Int, journalBufferTimeout: Int?, jmxEnabled: Boolean, baseDirectory: Path, shouldStartLocalShell: Boolean)</ID>
<ID>LongParameterList:ArtemisRpcBroker.kt$ArtemisRpcBroker.Companion$(configuration: MutualSslConfiguration, address: NetworkHostAndPort, adminAddress: NetworkHostAndPort, 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&lt;Attachment&gt;, params: NetworkParameters, txId: SecureHash, isAttachmentTrusted: (Attachment) -&gt; Boolean, parent: ClassLoader = ClassLoader.getSystemClassLoader(), block: (ClassLoader) -&gt; T)</ID>
<ID>LongParameterList:BFTSmart.kt$BFTSmart.Replica$( states: List&lt;StateRef&gt;, txId: SecureHash, callerName: CordaX500Name, requestSignature: NotarisationRequestSignature, timeWindow: TimeWindow?, references: List&lt;StateRef&gt; = emptyList() )</ID>
<ID>LongParameterList:BusinessCalendar.kt$BusinessCalendar.Companion$(startDate: LocalDate, period: Frequency, calendar: BusinessCalendar = EMPTY, dateRollConvention: DateRollConvention = DateRollConvention.Following, noOfAdditionalPeriods: Int = Integer.MAX_VALUE, endDate: LocalDate? = null, periodOffset: Int? = null)</ID>
<ID>LongParameterList:Cash.kt$Cash$(inputs: List&lt;State&gt;, outputs: List&lt;State&gt;, tx: LedgerTransaction, issueCommand: CommandWithParties&lt;Commands.Issue&gt;, currency: Currency, issuer: PartyAndReference)</ID>
@ -719,7 +718,6 @@
<ID>LongParameterList:X509Utilities.kt$X509Utilities$(certificateType: CertificateType, issuer: X500Principal, issuerPublicKey: PublicKey, issuerSigner: ContentSigner, subject: X500Principal, subjectPublicKey: PublicKey, validityWindow: Pair&lt;Date, Date&gt;, nameConstraints: NameConstraints? = null, crlDistPoint: String? = null, crlIssuer: X500Name? = null)</ID>
<ID>LongParameterList:X509Utilities.kt$X509Utilities$(certificateType: CertificateType, issuer: X500Principal, issuerPublicKey: PublicKey, subject: X500Principal, subjectPublicKey: PublicKey, validityWindow: Pair&lt;Date, Date&gt;, nameConstraints: NameConstraints? = null, crlDistPoint: String? = null, crlIssuer: X500Name? = null)</ID>
<ID>LongParameterList:X509Utilities.kt$X509Utilities$(certificateType: CertificateType, issuerCertificate: X509Certificate, issuerKeyPair: KeyPair, subject: X500Principal, subjectPublicKey: PublicKey, validityWindow: Pair&lt;Duration, Duration&gt; = DEFAULT_VALIDITY_WINDOW, nameConstraints: NameConstraints? = null, crlDistPoint: String? = null, crlIssuer: X500Name? = null)</ID>
<ID>LongParameterList:internalAccessTestHelpers.kt$( inputs: List&lt;StateAndRef&lt;ContractState&gt;&gt;, outputs: List&lt;TransactionState&lt;ContractState&gt;&gt;, commands: List&lt;CommandWithParties&lt;CommandData&gt;&gt;, attachments: List&lt;Attachment&gt;, id: SecureHash, notary: Party?, timeWindow: TimeWindow?, privacySalt: PrivacySalt, networkParameters: NetworkParameters, references: List&lt;StateAndRef&lt;ContractState&gt;&gt;, componentGroups: List&lt;ComponentGroup&gt;? = null, serializedInputs: List&lt;SerializedStateAndRef&gt;? = null, serializedReferences: List&lt;SerializedStateAndRef&gt;? = null, isAttachmentTrusted: (Attachment) -&gt; Boolean )</ID>
<ID>MagicNumber:AMQPClientSerializationScheme.kt$AMQPClientSerializationScheme.Companion$128</ID>
<ID>MagicNumber: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&lt;ContractClassName, ContractAttachment&gt;</ID>
<ID>ThrowsCount:TransactionVerifierServiceInternal.kt$Verifier$// Using basic graph theory, a full cycle of encumbered (co-dependent) states should exist to achieve bi-directional // encumbrances. This property is important to ensure that no states involved in an encumbrance-relationship // can be spent on their own. Briefly, if any of the states is having more than one encumbrance references by // other states, a full cycle detection will fail. As a result, all of the encumbered states must be present // as "from" and "to" only once (or zero times if no encumbrance takes place). For instance, // a -&gt; b // c -&gt; b and a -&gt; b // b -&gt; a b -&gt; c // do not satisfy the bi-directionality (full cycle) property. // // In the first example "b" appears twice in encumbrance ("to") list and "c" exists in the encumbered ("from") list only. // Due the above, one could consume "a" and "b" in the same transaction and then, because "b" is already consumed, "c" cannot be spent. // // Similarly, the second example does not form a full cycle because "a" and "c" exist in one of the lists only. // As a result, one can consume "b" and "c" in the same transactions, which will make "a" impossible to be spent. // // On other hand the following are valid constructions: // a -&gt; b a -&gt; c // b -&gt; c and c -&gt; b // c -&gt; a b -&gt; a // and form a full cycle, meaning that the bi-directionality property is satisfied. private fun checkBidirectionalOutputEncumbrances(statesAndEncumbrance: List&lt;Pair&lt;Int, Int&gt;&gt;)</ID>
<ID>ThrowsCount:WireTransaction.kt$WireTransaction$private fun toLedgerTransactionInternal( resolveIdentity: (PublicKey) -&gt; Party?, resolveAttachment: (SecureHash) -&gt; Attachment?, resolveStateRefAsSerialized: (StateRef) -&gt; SerializedBytes&lt;TransactionState&lt;ContractState&gt;&gt;?, resolveParameters: (SecureHash?) -&gt; NetworkParameters?, isAttachmentTrusted: (Attachment) -&gt; Boolean ): LedgerTransaction</ID>
<ID>ThrowsCount:WireTransaction.kt$WireTransaction.Companion$ @CordaInternal fun resolveStateRefBinaryComponent(stateRef: StateRef, services: ServicesForResolution): SerializedBytes&lt;TransactionState&lt;ContractState&gt;&gt;?</ID>
<ID>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>

Binary file not shown.

View File

@ -15,7 +15,6 @@ import net.corda.core.crypto.Crypto.generateKeyPair
import net.corda.core.crypto.SignatureScheme
import net.corda.core.crypto.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) }
}

View File

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

View File

@ -2,12 +2,11 @@ package net.corda.nodeapitests.internal.persistence
import net.corda.core.schemas.MappedSchema
import net.corda.core.schemas.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)
}
}

View File

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

View File

@ -75,6 +75,15 @@ constructor(private val initSerEnv: Boolean,
"generate-node-info"
)
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

View File

@ -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)
}
}

View File

@ -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()

View File

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

View File

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

View File

@ -4,44 +4,40 @@ import com.fasterxml.jackson.databind.ObjectMapper
import liquibase.Contexts
import liquibase.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"
}
}

View File

@ -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) }
}

View File

@ -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
}

View File

@ -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())

View File

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

View File

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

View File

@ -37,7 +37,8 @@ class FlowCheckpointVersionNodeStartupCheckTest {
startNodesInProcess = false,
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"

View File

@ -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()

View File

@ -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),

View File

@ -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()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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()

View File

@ -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()

View File

@ -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"

View File

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

View File

@ -34,9 +34,11 @@ import net.corda.testing.node.internal.FINANCE_CORDAPPS
import net.corda.testing.node.internal.enclosedCordapp
import 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)
}
}

View File

@ -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)) {

View File

@ -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()
}
}
}
}

View File

@ -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)
}
}
}

View File

@ -58,7 +58,7 @@ class RpcExceptionHandlingTest {
}
}
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = listOf(enclosedCordapp()))) {
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = listOf(enclosedCordapp()), allowHibernateToManageAppSchema = false)) {
val (devModeNode, node) = listOf(startNode(params, BOB_NAME),
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 ->

View File

@ -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()
}
}
}

View File

@ -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.

View File

@ -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) })

View File

@ -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)

View File

@ -109,6 +109,8 @@ import net.corda.node.services.messaging.DeduplicationHandler
import net.corda.node.services.messaging.MessagingService
import net.corda.node.services.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) {

View File

@ -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()

View File

@ -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) {

View File

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

View File

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

View File

@ -10,9 +10,11 @@ import net.corda.core.identity.CordaX500Name
import net.corda.core.schemas.MappedSchema
import net.corda.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? {

View File

@ -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)
}

View File

@ -15,7 +15,6 @@ import net.corda.nodeapi.internal.config.MutualSslConfiguration
import net.corda.nodeapi.internal.config.SslConfiguration
import net.corda.nodeapi.internal.config.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
)
}

View File

@ -14,6 +14,7 @@ import net.corda.common.validation.internal.Validated.Companion.invalid
import net.corda.common.validation.internal.Validated.Companion.valid
import net.corda.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]))
}
}

View File

@ -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 })
}
}

View File

@ -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
}
}

View File

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

View File

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

View File

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

View File

@ -38,9 +38,10 @@ import javax.persistence.PersistenceException
/** Database-based network map cache. */
@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
}
}

View File

@ -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()

View File

@ -95,7 +95,9 @@ class DBTransactionStorage(private val database: CordaPersistence, cacheFactory:
}
}
private companion object {
internal companion object {
const val TRANSACTION_ALREADY_IN_PROGRESS_WARNING = "trackTransaction is called with an already existing, open DB transaction. As a result, there might be transactions missing from the returned data feed, because of race conditions."
// Rough estimate for the average of a public key and the transaction metadata - hard to get exact figures here,
// 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> {

View File

@ -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.

View File

@ -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.
*/

View File

@ -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() {

View File

@ -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.

View File

@ -19,22 +19,25 @@ import net.corda.core.utilities.contextLogger
import net.corda.node.services.api.CheckpointStorage
import net.corda.node.services.api.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
)
}
}
}

View File

@ -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
}
/**

View File

@ -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(

View File

@ -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,

View File

@ -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)

View File

@ -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")

View File

@ -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()
}
}

View File

@ -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
}
}
}

View File

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

View File

@ -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 {

View File

@ -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)

View File

@ -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)

View File

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

View File

@ -645,8 +645,8 @@ class DBCheckpointStorageTests {
val (extractedId, extractedCheckpoint) = checkpointStorage.getPausedCheckpoints().toList().single()
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)

View File

@ -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())) {

View File

@ -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

View File

@ -48,6 +48,7 @@ import net.corda.testing.internal.vault.VaultFiller
import net.corda.testing.node.MockServices
import net.corda.testing.node.MockServices.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
}

View File

@ -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")
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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)),

View File

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

View File

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

View File

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

View File

@ -81,6 +81,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar', nodeTask])
cordapp project(':finance:workflows')
cordapp project(':finance: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