From a33309a31bd2fc93a936eb060335fda66839dabf Mon Sep 17 00:00:00 2001 From: Adel El-Beik <48713346+adelel1@users.noreply.github.com> Date: Wed, 27 May 2020 11:35:15 +0100 Subject: [PATCH 01/85] CORDA-3755: Backport AttachmentURLStreamHandlerFactory memory leak (#6274) * CORDA-3755: Switched attachments map to a WeakHashMap (#6214) * CORDA-3772: Now specify source and target of 8 when compiling contract classes. * CORDA-3651: addManifest now uses separate files for reading and writing. (#6026) * CORDA-3651: addManifest now uses separate files for reading and writing. * CORDA-3651: The jar scanning loader now closes itsself. Co-authored-by: Adel El-Beik Co-authored-by: Adel El-Beik --- core-deterministic/build.gradle | 1 + .../internal/AttachmentsHolderImpl.kt | 23 ++++ .../AttachmentsClassLoaderTests.kt | 126 +++++++++++++++++- .../internal/AttachmentsClassLoader.kt | 79 ++++++++--- .../core/internal/ContractJarTestUtils.kt | 12 +- .../core/internal/JarSignatureTestUtils.kt | 5 +- 6 files changed, 218 insertions(+), 28 deletions(-) create mode 100644 core-deterministic/src/main/kotlin/net/corda/core/serialization/internal/AttachmentsHolderImpl.kt diff --git a/core-deterministic/build.gradle b/core-deterministic/build.gradle index 4b84f32d7e..5d7d1cd971 100644 --- a/core-deterministic/build.gradle +++ b/core-deterministic/build.gradle @@ -59,6 +59,7 @@ task patchCore(type: Zip, dependsOn: coreJarTask) { from(zipTree(originalJar)) { exclude 'net/corda/core/internal/*ToggleField*.class' exclude 'net/corda/core/serialization/*SerializationFactory*.class' + exclude 'net/corda/core/serialization/internal/AttachmentsHolderImpl.class' exclude 'net/corda/core/serialization/internal/CheckpointSerializationFactory*.class' exclude 'net/corda/core/internal/rules/*.class' } diff --git a/core-deterministic/src/main/kotlin/net/corda/core/serialization/internal/AttachmentsHolderImpl.kt b/core-deterministic/src/main/kotlin/net/corda/core/serialization/internal/AttachmentsHolderImpl.kt new file mode 100644 index 0000000000..a2f7b8ab30 --- /dev/null +++ b/core-deterministic/src/main/kotlin/net/corda/core/serialization/internal/AttachmentsHolderImpl.kt @@ -0,0 +1,23 @@ +package net.corda.core.serialization.internal + +import net.corda.core.contracts.Attachment +import java.net.URL + +@Suppress("unused") +private class AttachmentsHolderImpl : AttachmentsHolder { + private val attachments = LinkedHashMap>() + + override val size: Int get() = attachments.size + + override fun getKey(key: URL): URL? { + return attachments[key]?.first + } + + override fun get(key: URL): Attachment? { + return attachments[key]?.second + } + + override fun set(key: URL, value: Attachment) { + attachments[key] = key to value + } +} diff --git a/core-tests/src/test/kotlin/net/corda/coretests/transactions/AttachmentsClassLoaderTests.kt b/core-tests/src/test/kotlin/net/corda/coretests/transactions/AttachmentsClassLoaderTests.kt index 9007a7e28d..3bcc21dd9a 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/transactions/AttachmentsClassLoaderTests.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/transactions/AttachmentsClassLoaderTests.kt @@ -1,19 +1,37 @@ package net.corda.coretests.transactions +import net.corda.core.contracts.AlwaysAcceptAttachmentConstraint import net.corda.core.contracts.Attachment +import net.corda.core.contracts.CommandData +import net.corda.core.contracts.CommandWithParties import net.corda.core.contracts.Contract +import net.corda.core.contracts.ContractAttachment +import net.corda.core.contracts.PrivacySalt +import net.corda.core.contracts.StateAndRef +import net.corda.core.contracts.TimeWindow +import net.corda.core.contracts.TransactionState import net.corda.core.contracts.TransactionVerificationException import net.corda.core.crypto.Crypto import net.corda.core.crypto.SecureHash +import net.corda.core.identity.Party +import net.corda.core.internal.AbstractAttachment import net.corda.core.internal.AttachmentTrustCalculator +import net.corda.core.internal.createLedgerTransaction import net.corda.core.internal.declaredField import net.corda.core.internal.hash import net.corda.core.internal.inputStream import net.corda.core.node.NetworkParameters import net.corda.core.node.services.AttachmentId import net.corda.core.serialization.internal.AttachmentsClassLoader -import net.corda.node.services.attachments.NodeAttachmentTrustCalculator import net.corda.testing.common.internal.testNetworkParameters +import net.corda.node.services.attachments.NodeAttachmentTrustCalculator +import net.corda.testing.contracts.DummyContract +import net.corda.testing.core.ALICE_NAME +import net.corda.testing.core.BOB_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.internal.ContractJarTestUtils import net.corda.testing.core.internal.ContractJarTestUtils.signContractJar import net.corda.testing.internal.TestingNamedCacheFactory import net.corda.testing.internal.fakeAttachment @@ -24,10 +42,14 @@ import org.apache.commons.io.IOUtils import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals import org.junit.Before +import org.junit.Rule import org.junit.Test +import org.junit.rules.TemporaryFolder import java.io.ByteArrayOutputStream import java.io.InputStream import java.net.URL +import java.nio.file.Path +import java.security.PublicKey import kotlin.test.assertFailsWith class AttachmentsClassLoaderTests { @@ -43,8 +65,21 @@ class AttachmentsClassLoaderTests { it.toByteArray() } } + val ALICE = TestIdentity(ALICE_NAME, 70).party + val BOB = TestIdentity(BOB_NAME, 80).party + val dummyNotary = TestIdentity(DUMMY_NOTARY_NAME, 20) + val DUMMY_NOTARY get() = dummyNotary.party + val PROGRAM_ID: String = "net.corda.testing.contracts.MyDummyContract" } + @Rule + @JvmField + val tempFolder = TemporaryFolder() + + @Rule + @JvmField + val testSerialization = SerializationEnvironmentRule(true) + private lateinit var storage: MockAttachmentStorage private lateinit var internalStorage: InternalMockAttachmentStorage private lateinit var attachmentTrustCalculator: AttachmentTrustCalculator @@ -448,4 +483,93 @@ class AttachmentsClassLoaderTests { createClassloader(trustedAttachment) } + + @Test(timeout=300_000) + fun `attachment still available in verify after forced gc in verify`() { + tempFolder.root.toPath().let { path -> + val baseOutState = TransactionState(DummyContract.SingleOwnerState(0, ALICE), PROGRAM_ID, DUMMY_NOTARY, constraint = AlwaysAcceptAttachmentConstraint) + val inputs = emptyList>() + val outputs = listOf(baseOutState, baseOutState.copy(notary = ALICE), baseOutState.copy(notary = BOB)) + val commands = emptyList>() + + val content = createContractString(PROGRAM_ID) + val contractJarPath = ContractJarTestUtils.makeTestContractJar(path, PROGRAM_ID, content = content) + + val attachments = createAttachments(contractJarPath) + + val id = SecureHash.randomSHA256() + val timeWindow: TimeWindow? = null + val privacySalt = PrivacySalt() + val transaction = createLedgerTransaction( + inputs, + outputs, + commands, + attachments, + id, + null, + timeWindow, + privacySalt, + testNetworkParameters(), + emptyList(), + isAttachmentTrusted = { true } + ) + transaction.verify() + } + } + + private fun createContractString(contractName: String, versionSeed: Int = 0): String { + val pkgs = contractName.split(".") + val className = pkgs.last() + val packages = pkgs.subList(0, pkgs.size - 1) + + val output = """package ${packages.joinToString(".")}; + import net.corda.core.contracts.*; + import net.corda.core.transactions.*; + import java.net.URL; + import java.io.InputStream; + + public class $className implements Contract { + private int seed = $versionSeed; + @Override + public void verify(LedgerTransaction tx) throws IllegalArgumentException { + System.gc(); + InputStream str = this.getClass().getClassLoader().getResourceAsStream("importantDoc.pdf"); + if (str == null) throw new IllegalStateException("Could not find importantDoc.pdf"); + } + } + """.trimIndent() + + System.out.println(output) + return output + } + + private fun createAttachments(contractJarPath: Path) : List { + + val attachment = object : AbstractAttachment({contractJarPath.inputStream().readBytes()}, uploader = "app") { + @Suppress("OverridingDeprecatedMember") + override val signers: List = emptyList() + override val signerKeys: List = emptyList() + override val size: Int = 1234 + override val id: SecureHash = SecureHash.sha256(attachmentData) + } + val contractAttachment = ContractAttachment(attachment, PROGRAM_ID) + + return listOf( + object : AbstractAttachment({ISOLATED_CONTRACTS_JAR_PATH.openStream().readBytes()}, uploader = "app") { + @Suppress("OverridingDeprecatedMember") + override val signers: List = emptyList() + override val signerKeys: List = emptyList() + override val size: Int = 1234 + override val id: SecureHash = SecureHash.sha256(attachmentData) + }, + object : AbstractAttachment({fakeAttachment("importantDoc.pdf", "I am a pdf!").inputStream().readBytes() + }, uploader = "app") { + @Suppress("OverridingDeprecatedMember") + override val signers: List = emptyList() + override val signerKeys: List = emptyList() + override val size: Int = 1234 + override val id: SecureHash = SecureHash.sha256(attachmentData) + }, + contractAttachment) + } } diff --git a/core/src/main/kotlin/net/corda/core/serialization/internal/AttachmentsClassLoader.kt b/core/src/main/kotlin/net/corda/core/serialization/internal/AttachmentsClassLoader.kt index e85e5f838f..a0f6f6c141 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/internal/AttachmentsClassLoader.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/internal/AttachmentsClassLoader.kt @@ -17,6 +17,7 @@ import net.corda.core.utilities.debug import java.io.ByteArrayOutputStream import java.io.IOException import java.io.InputStream +import java.lang.ref.WeakReference import java.net.* import java.util.* import java.util.jar.JarInputStream @@ -53,14 +54,6 @@ class AttachmentsClassLoader(attachments: List, private val ignoreDirectories = listOf("org/jolokia/", "org/json/simple/") private val ignorePackages = ignoreDirectories.map { it.replace("/", ".") } - @VisibleForTesting - private fun readAttachment(attachment: Attachment, filepath: String): ByteArray { - ByteArrayOutputStream().use { - attachment.extractFile(filepath, it) - return it.toByteArray() - } - } - /** * Apply our custom factory either directly, if `URL.setURLStreamHandlerFactory` has not been called yet, * or use a decorator and reflection to bypass the single-call-per-JVM restriction otherwise. @@ -354,8 +347,7 @@ object AttachmentsClassLoaderBuilder { object AttachmentURLStreamHandlerFactory : URLStreamHandlerFactory { private const val attachmentScheme = "attachment" - // TODO - what happens if this grows too large? - private val loadedAttachments = mutableMapOf().toSynchronised() + private val loadedAttachments: AttachmentsHolder = AttachmentsHolderImpl() override fun createURLStreamHandler(protocol: String): URLStreamHandler? { return if (attachmentScheme == protocol) { @@ -363,25 +355,70 @@ object AttachmentURLStreamHandlerFactory : URLStreamHandlerFactory { } else null } + @Synchronized fun toUrl(attachment: Attachment): URL { - val id = attachment.id.toString() - loadedAttachments[id] = attachment - return URL(attachmentScheme, "", -1, id, AttachmentURLStreamHandler) + val proposedURL = URL(attachmentScheme, "", -1, attachment.id.toString(), AttachmentURLStreamHandler) + val existingURL = loadedAttachments.getKey(proposedURL) + return if (existingURL == null) { + loadedAttachments[proposedURL] = attachment + proposedURL + } else { + existingURL + } } + @VisibleForTesting + fun loadedAttachmentsSize(): Int = loadedAttachments.size + private object AttachmentURLStreamHandler : URLStreamHandler() { override fun openConnection(url: URL): URLConnection { if (url.protocol != attachmentScheme) throw IOException("Cannot handle protocol: ${url.protocol}") - val attachment = loadedAttachments[url.path] ?: throw IOException("Could not load url: $url .") + val attachment = loadedAttachments[url] ?: throw IOException("Could not load url: $url .") return AttachmentURLConnection(url, attachment) } - } - private class AttachmentURLConnection(url: URL, private val attachment: Attachment) : URLConnection(url) { - override fun getContentLengthLong(): Long = attachment.size.toLong() - override fun getInputStream(): InputStream = attachment.open() - override fun connect() { - connected = true + override fun equals(attachmentUrl: URL, otherURL: URL?): Boolean { + if (attachmentUrl.protocol != otherURL?.protocol) return false + if (attachmentUrl.protocol != attachmentScheme) throw IllegalArgumentException("Cannot handle protocol: ${attachmentUrl.protocol}") + return attachmentUrl.file == otherURL?.file + } + + override fun hashCode(url: URL): Int { + if (url.protocol != attachmentScheme) throw IllegalArgumentException("Cannot handle protocol: ${url.protocol}") + return url.file.hashCode() + } + + private class AttachmentURLConnection(url: URL, private val attachment: Attachment) : URLConnection(url) { + override fun getContentLengthLong(): Long = attachment.size.toLong() + override fun getInputStream(): InputStream = attachment.open() + override fun connect() { + connected = true + } } } -} \ No newline at end of file +} + +interface AttachmentsHolder { + val size: Int + fun getKey(key: URL): URL? + operator fun get(key: URL): Attachment? + operator fun set(key: URL, value: Attachment) +} + +private class AttachmentsHolderImpl : AttachmentsHolder { + private val attachments = WeakHashMap, Attachment>>().toSynchronised() + + override val size: Int get() = attachments.size + + override fun getKey(key: URL): URL? { + return attachments[key]?.first?.get() + } + + override fun get(key: URL): Attachment? { + return attachments[key]?.second + } + + override fun set(key: URL, value: Attachment) { + attachments[key] = WeakReference(key) to value + } +} diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/core/internal/ContractJarTestUtils.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/core/internal/ContractJarTestUtils.kt index cefc596c3a..3ba57fa20a 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/core/internal/ContractJarTestUtils.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/core/internal/ContractJarTestUtils.kt @@ -59,12 +59,14 @@ object ContractJarTestUtils { return workingDir.resolve(jarName) to signer } + @Suppress("LongParameterList") @JvmOverloads - fun makeTestContractJar(workingDir: Path, contractName: String, signed: Boolean = false, version: Int = 1, versionSeed: Int = 0): Path { + fun makeTestContractJar(workingDir: Path, contractName: String, signed: Boolean = false, version: Int = 1, versionSeed: Int = 0, + content: String? = null): Path { val packages = contractName.split(".") val jarName = "attachment-${packages.last()}-$version-$versionSeed-${(if (signed) "signed" else "")}.jar" val className = packages.last() - createTestClass(workingDir, className, packages.subList(0, packages.size - 1), versionSeed) + createTestClass(workingDir, className, packages.subList(0, packages.size - 1), versionSeed, content) workingDir.createJar(jarName, "${contractName.replace(".", "/")}.class") workingDir.addManifest(jarName, Pair(Attributes.Name(CORDAPP_CONTRACT_VERSION), version.toString())) return workingDir.resolve(jarName) @@ -87,8 +89,8 @@ object ContractJarTestUtils { return workingDir.resolve(jarName) } - private fun createTestClass(workingDir: Path, className: String, packages: List, versionSeed: Int = 0): Path { - val newClass = """package ${packages.joinToString(".")}; + private fun createTestClass(workingDir: Path, className: String, packages: List, versionSeed: Int = 0, content: String? = null): Path { + val newClass = content ?: """package ${packages.joinToString(".")}; import net.corda.core.contracts.*; import net.corda.core.transactions.*; @@ -108,7 +110,7 @@ object ContractJarTestUtils { val fileManager = compiler.getStandardFileManager(null, null, null) fileManager.setLocation(StandardLocation.CLASS_OUTPUT, listOf(workingDir.toFile())) - compiler.getTask(System.out.writer(), fileManager, null, null, null, listOf(source)).call() + compiler.getTask(System.out.writer(), fileManager, null, listOf("-source", "8", "-target", "8"), null, listOf(source)).call() val outFile = fileManager.getFileForInput(StandardLocation.CLASS_OUTPUT, packages.joinToString("."), "$className.class") return Paths.get(outFile.name) } diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/core/internal/JarSignatureTestUtils.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/core/internal/JarSignatureTestUtils.kt index 00a827c1e1..4c21340f84 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/core/internal/JarSignatureTestUtils.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/core/internal/JarSignatureTestUtils.kt @@ -12,6 +12,7 @@ import java.nio.file.Files import java.nio.file.NoSuchFileException import java.nio.file.Path import java.nio.file.Paths +import java.nio.file.StandardCopyOption.REPLACE_EXISTING import java.security.PublicKey import java.util.jar.Attributes import java.util.jar.JarInputStream @@ -88,12 +89,13 @@ object JarSignatureTestUtils { JarInputStream(FileInputStream((this / fileName).toFile())).use(JarSignatureCollector::collectSigners) fun Path.addManifest(fileName: String, vararg entries: Pair) { + val outputFile = this / (fileName + "Output") JarInputStream(FileInputStream((this / fileName).toFile())).use { input -> val manifest = input.manifest ?: Manifest() entries.forEach { (attributeName, value) -> manifest.mainAttributes[attributeName] = value } - val output = JarOutputStream(FileOutputStream((this / fileName).toFile()), manifest) + val output = JarOutputStream(FileOutputStream(outputFile.toFile()), manifest) var entry = input.nextEntry val buffer = ByteArray(1 shl 14) while (true) { @@ -108,5 +110,6 @@ object JarSignatureTestUtils { } output.close() } + Files.copy(outputFile, this / fileName, REPLACE_EXISTING) } } From d7e55ed4e5e866fcda8ebb6bc6d8ae27ae2df630 Mon Sep 17 00:00:00 2001 From: Joseph Zuniga-Daly <59851625+josephzunigadaly@users.noreply.github.com> Date: Thu, 11 Jun 2020 11:14:51 +0100 Subject: [PATCH 02/85] CORDA-3837: When delivering a lifecycle event, copy the database context into the lifecycle event thread (#6331) --- .../internal/lifecycle/NodeLifecycleEventsDistributor.kt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/lifecycle/NodeLifecycleEventsDistributor.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/lifecycle/NodeLifecycleEventsDistributor.kt index 4672ff85ac..6c26c5e0a1 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/lifecycle/NodeLifecycleEventsDistributor.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/lifecycle/NodeLifecycleEventsDistributor.kt @@ -7,6 +7,8 @@ import net.corda.core.internal.concurrent.openFuture import net.corda.core.node.services.CordaServiceCriticalFailureException import net.corda.core.utilities.Try import net.corda.core.utilities.contextLogger +import net.corda.nodeapi.internal.persistence.contextDatabase +import net.corda.nodeapi.internal.persistence.contextDatabaseOrNull import java.io.Closeable import java.util.Collections.singleton import java.util.LinkedList @@ -93,7 +95,14 @@ class NodeLifecycleEventsDistributor : Closeable { log.warn("Not distributing $event as executor been already shutdown. Double close() case?") result.set(null) } else { + + val passTheDbToTheThread = contextDatabaseOrNull + executor.execute { + + if (passTheDbToTheThread != null) + contextDatabase = passTheDbToTheThread + val orderedSnapshot = if (event.reversedPriority) snapshot.reversed() else snapshot orderedSnapshot.forEach { log.debug("Distributing event $event to: $it") From 2b7c220522003099beec1a3b49b92c089ec9aa9a Mon Sep 17 00:00:00 2001 From: Razvan Codreanu <52859362+Schife@users.noreply.github.com> Date: Fri, 12 Jun 2020 15:30:15 +0100 Subject: [PATCH 03/85] INFRA-387 preallocation and deployNodes migration to Code Checks (#6339) * preallocate for slow integration tests as well --- .ci/dev/nightly-regression/Jenkinsfile | 82 ++++++++++++++------------ .ci/dev/pr-code-checks/Jenkinsfile | 6 ++ .ci/dev/regression/Jenkinsfile | 4 +- Jenkinsfile | 2 +- 4 files changed, 53 insertions(+), 41 deletions(-) diff --git a/.ci/dev/nightly-regression/Jenkinsfile b/.ci/dev/nightly-regression/Jenkinsfile index a122ea0fd3..a7f5d5ba17 100644 --- a/.ci/dev/nightly-regression/Jenkinsfile +++ b/.ci/dev/nightly-regression/Jenkinsfile @@ -23,51 +23,57 @@ pipeline { } stages { - stage('Corda Pull Request - Generate Build Image') { - steps { - withCredentials([string(credentialsId: 'container_reg_passwd', variable: 'DOCKER_PUSH_PWD')]) { - sh "./gradlew " + - "-Dkubenetize=true " + - "-Ddocker.push.password=\"\${DOCKER_PUSH_PWD}\" " + - "-Ddocker.work.dir=\"/tmp/\${EXECUTOR_NUMBER}\" " + - "-Ddocker.build.tag=\"\${DOCKER_TAG_TO_USE}\"" + - " clean jar deployNodes install pushBuildImage --stacktrace" - } - sh "kubectl auth can-i get pods" - } + stage('Deploy Nodes') { + steps { + sh "./gradlew --no-daemon jar deployNodes" } + } - stage('Testing phase') { - parallel { - stage('Regression Test') { - steps { - sh "./gradlew " + - "-DbuildId=\"\${BUILD_ID}\" " + - "-Dkubenetize=true " + - "-Ddocker.run.tag=\"\${DOCKER_TAG_TO_USE}\" " + - "-Dartifactory.username=\"\${ARTIFACTORY_CREDENTIALS_USR}\" " + - "-Dartifactory.password=\"\${ARTIFACTORY_CREDENTIALS_PSW}\" " + - "-Dgit.branch=\"\${GIT_BRANCH}\" " + - "-Dgit.target.branch=\"\${GIT_BRANCH}\" " + - " parallelRegressionTest --stacktrace" - } + stage('Generate Build Image') { + steps { + withCredentials([string(credentialsId: 'container_reg_passwd', variable: 'DOCKER_PUSH_PWD')]) { + sh "./gradlew " + + "-Dkubenetize=true " + + "-Ddocker.push.password=\"\${DOCKER_PUSH_PWD}\" " + + "-Ddocker.work.dir=\"/tmp/\${EXECUTOR_NUMBER}\" " + + "-Ddocker.build.tag=\"\${DOCKER_TAG_TO_USE}\"" + + " clean pushBuildImage --stacktrace" + } + sh "kubectl auth can-i get pods" + } + } + + stage('Testing phase') { + parallel { + stage('Regression Test') { + steps { + sh "./gradlew " + + "-DbuildId=\"\${BUILD_ID}\" " + + "-Dkubenetize=true " + + "-Ddocker.run.tag=\"\${DOCKER_TAG_TO_USE}\" " + + "-Dartifactory.username=\"\${ARTIFACTORY_CREDENTIALS_USR}\" " + + "-Dartifactory.password=\"\${ARTIFACTORY_CREDENTIALS_PSW}\" " + + "-Dgit.branch=\"\${GIT_BRANCH}\" " + + "-Dgit.target.branch=\"\${GIT_BRANCH}\" " + + " parallelRegressionTest --stacktrace" } - stage('Slow Integration Test') { - steps { - sh "./gradlew " + - "-DbuildId=\"\${BUILD_ID}\" " + - "-Dkubenetize=true " + - "-Ddocker.run.tag=\"\${DOCKER_TAG_TO_USE}\" " + - "-Dartifactory.username=\"\${ARTIFACTORY_CREDENTIALS_USR}\" " + - "-Dartifactory.password=\"\${ARTIFACTORY_CREDENTIALS_PSW}\" " + - "-Dgit.branch=\"\${GIT_BRANCH}\" " + - "-Dgit.target.branch=\"\${GIT_BRANCH}\" " + - " allParallelSlowIntegrationTest --stacktrace" - } + } + stage('Slow Integration Test') { + steps { + sh "./gradlew " + + "-DbuildId=\"\${BUILD_ID}\" " + + "-Dkubenetize=true " + + "-Ddocker.run.tag=\"\${DOCKER_TAG_TO_USE}\" " + + "-Dartifactory.username=\"\${ARTIFACTORY_CREDENTIALS_USR}\" " + + "-Dartifactory.password=\"\${ARTIFACTORY_CREDENTIALS_PSW}\" " + + "-Dgit.branch=\"\${GIT_BRANCH}\" " + + "-Dgit.target.branch=\"\${GIT_BRANCH}\" " + + " allParallelSlowIntegrationTest --stacktrace" } } } } + } post { diff --git a/.ci/dev/pr-code-checks/Jenkinsfile b/.ci/dev/pr-code-checks/Jenkinsfile index 0fdd5b0055..c74639b4f6 100644 --- a/.ci/dev/pr-code-checks/Jenkinsfile +++ b/.ci/dev/pr-code-checks/Jenkinsfile @@ -40,6 +40,12 @@ pipeline { sh ".ci/check-api-changes.sh" } } + + stage('Deploy Nodes') { + steps { + sh "./gradlew --no-daemon jar deployNodes" + } + } } post { diff --git a/.ci/dev/regression/Jenkinsfile b/.ci/dev/regression/Jenkinsfile index 3cebf1840c..899f44684e 100644 --- a/.ci/dev/regression/Jenkinsfile +++ b/.ci/dev/regression/Jenkinsfile @@ -19,7 +19,7 @@ pipeline { } stages { - stage('Corda Pull Request - Generate Build Image') { + stage('Generate Build Image') { steps { withCredentials([string(credentialsId: 'container_reg_passwd', variable: 'DOCKER_PUSH_PWD')]) { sh "./gradlew " + @@ -27,7 +27,7 @@ pipeline { "-Ddocker.push.password=\"\${DOCKER_PUSH_PWD}\" " + "-Ddocker.work.dir=\"/tmp/\${EXECUTOR_NUMBER}\" " + "-Ddocker.build.tag=\"\${DOCKER_TAG_TO_USE}\"" + - " clean jar deployNodes install pushBuildImage --stacktrace" + " clean preAllocateForParallelRegressionTest preAllocateForAllSlowIntegrationTest pushBuildImage --stacktrace" } sh "kubectl auth can-i get pods" } diff --git a/Jenkinsfile b/Jenkinsfile index 18837765aa..8048bed1ae 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -27,7 +27,7 @@ pipeline { "-Ddocker.push.password=\"\${DOCKER_PUSH_PWD}\" " + "-Ddocker.work.dir=\"/tmp/\${EXECUTOR_NUMBER}\" " + "-Ddocker.build.tag=\"\${DOCKER_TAG_TO_USE}\"" + - " clean jar deployNodes pushBuildImage --stacktrace" + " clean preAllocateForAllParallelUnitTest preAllocateForAllParallelIntegrationTest pushBuildImage --stacktrace" } sh "kubectl auth can-i get pods" } From 2c253d806d39bbcabf0a9b94caef95daa9516627 Mon Sep 17 00:00:00 2001 From: James Higgs <45565019+JamesHR3@users.noreply.github.com> Date: Fri, 12 Jun 2020 16:52:01 +0100 Subject: [PATCH 04/85] EG-2684 Ensure original message is logged with old-style error codes (#6340) * [EG-2684] Ensure original message is logged with old-style error codes * [EG-2684] Address detekt issues --- .../logging/ExceptionsErrorCodeFunctions.kt | 3 +- .../ExceptionsErrorCodeFunctionsTest.kt | 34 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 common/logging/src/test/kotlin/net/corda/commmon/logging/ExceptionsErrorCodeFunctionsTest.kt diff --git a/common/logging/src/main/kotlin/net/corda/common/logging/ExceptionsErrorCodeFunctions.kt b/common/logging/src/main/kotlin/net/corda/common/logging/ExceptionsErrorCodeFunctions.kt index a34e436bb8..3064fce1cc 100644 --- a/common/logging/src/main/kotlin/net/corda/common/logging/ExceptionsErrorCodeFunctions.kt +++ b/common/logging/src/main/kotlin/net/corda/common/logging/ExceptionsErrorCodeFunctions.kt @@ -32,8 +32,9 @@ fun Message.withErrorCodeFor(error: Throwable?, level: Level): Message { return when { error != null && level.isInRange(Level.FATAL, Level.WARN) -> { + val logMessage = this.formattedMessage val message = error.walkExceptionCausedByList().asSequence().mapNotNull(Throwable::message).joinToString(" - ") - CompositeMessage("$message [errorCode=${error.errorCode()}, moreInformationAt=${error.errorCodeLocationUrl()}]", format, parameters, throwable) + CompositeMessage("$logMessage - $message [errorCode=${error.errorCode()}, moreInformationAt=${error.errorCodeLocationUrl()}]", format, parameters, throwable) } else -> this } diff --git a/common/logging/src/test/kotlin/net/corda/commmon/logging/ExceptionsErrorCodeFunctionsTest.kt b/common/logging/src/test/kotlin/net/corda/commmon/logging/ExceptionsErrorCodeFunctionsTest.kt new file mode 100644 index 0000000000..45b44abedb --- /dev/null +++ b/common/logging/src/test/kotlin/net/corda/commmon/logging/ExceptionsErrorCodeFunctionsTest.kt @@ -0,0 +1,34 @@ +package net.corda.commmon.logging + +import com.natpryce.hamkrest.assertion.assertThat +import com.natpryce.hamkrest.contains +import net.corda.common.logging.withErrorCodeFor +import org.apache.logging.log4j.Level +import org.apache.logging.log4j.message.SimpleMessage +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 + } + val exception = throwables.last() + val message = originalMessage.withErrorCodeFor(exception, Level.ERROR) + assertThat(message.formattedMessage, contains("This is a test message".toRegex())) + for (i in (0..10)) { + assertThat(message.formattedMessage, contains("This is exception $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) +} \ No newline at end of file From 26d4bfb89f2aff12f5d96f3a2aac8d9f747ae665 Mon Sep 17 00:00:00 2001 From: Tamas Veingartner Date: Tue, 16 Jun 2020 09:15:51 +0100 Subject: [PATCH 05/85] =?UTF-8?q?CORDA-3578=20add=20corda=20prefix=20to=20?= =?UTF-8?q?conf=20file=20names=20as=20original=20issue=20was=20=E2=80=A6?= =?UTF-8?q?=20(#6322)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * CORDA-3578 add corda prefix to conf file names as original issue was having non-corda reference.conf files on classpath causes DriverDSLImp failure it is better to have this naming convention and avoid further conflicts of conf files. * fixed weird duplicates * revert renaming changes for web-reference.conf and loadtest-reference.conf --- docker/src/main/kotlin/net.corda.core/ConfigExporter.kt | 2 +- node/capsule/build.gradle | 2 +- node/capsule/src/main/java/CordaCaplet.java | 2 +- .../kotlin/net/corda/node/services/config/ConfigUtilities.kt | 2 +- .../main/resources/{reference.conf => corda-reference.conf} | 0 testing/testserver/src/main/java/CordaWebserverCaplet.java | 2 +- testing/testserver/testcapsule/build.gradle | 2 +- .../src/main/kotlin/net/corda/demobench/model/NodeConfig.kt | 2 +- .../test/kotlin/net/corda/demobench/model/NodeConfigTest.kt | 4 ++-- .../main/kotlin/net/corda/networkbuilder/nodes/NodeBuilder.kt | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) rename node/src/main/resources/{reference.conf => corda-reference.conf} (100%) diff --git a/docker/src/main/kotlin/net.corda.core/ConfigExporter.kt b/docker/src/main/kotlin/net.corda.core/ConfigExporter.kt index 2f7356cd7f..d9b74434ba 100644 --- a/docker/src/main/kotlin/net.corda.core/ConfigExporter.kt +++ b/docker/src/main/kotlin/net.corda.core/ConfigExporter.kt @@ -51,7 +51,7 @@ class ConfigExporter { } fun Config.parseAsNodeConfigWithFallback(): Validated { - val referenceConfig = ConfigFactory.parseResources("reference.conf") + val referenceConfig = ConfigFactory.parseResources("corda-reference.conf") val nodeConfig = this .withValue("baseDirectory", ConfigValueFactory.fromAnyRef("/opt/corda")) .withFallback(referenceConfig) diff --git a/node/capsule/build.gradle b/node/capsule/build.gradle index 2760a54aed..78b1a48970 100644 --- a/node/capsule/build.gradle +++ b/node/capsule/build.gradle @@ -51,7 +51,7 @@ task buildCordaJAR(type: FatCapsule, dependsOn: [ applicationSource = files( nodeProject.configurations.runtimeClasspath, nodeProject.tasks.jar, - nodeProject.buildDir.toString() + '/resources/main/reference.conf', + nodeProject.buildDir.toString() + '/resources/main/corda-reference.conf', "$rootDir/config/dev/log4j2.xml", 'NOTICE' // Copy CDDL notice ) diff --git a/node/capsule/src/main/java/CordaCaplet.java b/node/capsule/src/main/java/CordaCaplet.java index 76be8aad17..7633ba0a77 100644 --- a/node/capsule/src/main/java/CordaCaplet.java +++ b/node/capsule/src/main/java/CordaCaplet.java @@ -37,7 +37,7 @@ public class CordaCaplet extends Capsule { File configFile = (config == null) ? new File(baseDir, "node.conf") : new File(config); try { ConfigParseOptions parseOptions = ConfigParseOptions.defaults().setAllowMissing(false); - Config defaultConfig = ConfigFactory.parseResources("reference.conf", parseOptions); + Config defaultConfig = ConfigFactory.parseResources("corda-reference.conf", parseOptions); Config baseDirectoryConfig = ConfigFactory.parseMap(Collections.singletonMap("baseDirectory", baseDir)); Config nodeConfig = ConfigFactory.parseFile(configFile, parseOptions); return baseDirectoryConfig.withFallback(nodeConfig).withFallback(defaultConfig).resolve(); diff --git a/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt b/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt index 8a5eec792f..6aaac65251 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt @@ -42,7 +42,7 @@ object ConfigHelper { allowMissingConfig: Boolean = false, configOverrides: Config = ConfigFactory.empty()): Config { val parseOptions = ConfigParseOptions.defaults() - val defaultConfig = ConfigFactory.parseResources("reference.conf", parseOptions.setAllowMissing(false)) + val defaultConfig = ConfigFactory.parseResources("corda-reference.conf", parseOptions.setAllowMissing(false)) val appConfig = ConfigFactory.parseFile(configFile.toFile(), parseOptions.setAllowMissing(allowMissingConfig)) // Detect the underlying OS. If mac or windows non-server then we assume we're running in devMode. Unless specified otherwise. diff --git a/node/src/main/resources/reference.conf b/node/src/main/resources/corda-reference.conf similarity index 100% rename from node/src/main/resources/reference.conf rename to node/src/main/resources/corda-reference.conf diff --git a/testing/testserver/src/main/java/CordaWebserverCaplet.java b/testing/testserver/src/main/java/CordaWebserverCaplet.java index 61ada57e9b..ba0fbb7054 100644 --- a/testing/testserver/src/main/java/CordaWebserverCaplet.java +++ b/testing/testserver/src/main/java/CordaWebserverCaplet.java @@ -27,7 +27,7 @@ public class CordaWebserverCaplet extends Capsule { File configFile = (config == null) ? new File(baseDir, "node.conf") : new File(config); try { ConfigParseOptions parseOptions = ConfigParseOptions.defaults().setAllowMissing(false); - Config defaultConfig = ConfigFactory.parseResources("reference.conf", parseOptions); + Config defaultConfig = ConfigFactory.parseResources("corda-reference.conf", parseOptions); Config baseDirectoryConfig = ConfigFactory.parseMap(Collections.singletonMap("baseDirectory", baseDir)); Config nodeConfig = ConfigFactory.parseFile(configFile, parseOptions); return baseDirectoryConfig.withFallback(nodeConfig).withFallback(defaultConfig).resolve(); diff --git a/testing/testserver/testcapsule/build.gradle b/testing/testserver/testcapsule/build.gradle index b93547a028..f69d05b263 100644 --- a/testing/testserver/testcapsule/build.gradle +++ b/testing/testserver/testcapsule/build.gradle @@ -35,7 +35,7 @@ task buildWebserverJar(type: FatCapsule, dependsOn: project(':node').tasks.jar) project(':testing:testserver').tasks.jar, project(':testing:testserver').sourceSets.main.java.outputDir.toString() + '/CordaWebserverCaplet.class', project(':testing:testserver').sourceSets.main.java.outputDir.toString() + '/CordaWebserverCaplet$1.class', - project(':node').buildDir.toString() + '/resources/main/reference.conf', + project(':node').buildDir.toString() + '/resources/main/corda-reference.conf', "$rootDir/config/dev/log4j2.xml", project(':node:capsule').projectDir.toString() + '/NOTICE' // Copy CDDL notice ) diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt index 12be389088..86787676f5 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt @@ -19,7 +19,7 @@ import java.nio.file.StandardCopyOption import java.util.Properties /** - * This is a subset of FullNodeConfiguration, containing only those configs which we need. The node uses reference.conf + * This is a subset of FullNodeConfiguration, containing only those configs which we need. The node uses corda-reference.conf * to fill in the defaults so we're not required to specify them here. */ data class NodeConfig( diff --git a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt index e63c9a6ad6..05171410cf 100644 --- a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt +++ b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt @@ -37,7 +37,7 @@ class NodeConfigTest { val nodeConfig = config.nodeConf() .withValue("baseDirectory", valueFor(baseDir.toString())) - .withFallback(ConfigFactory.parseResources("reference.conf")) + .withFallback(ConfigFactory.parseResources("corda-reference.conf")) .withFallback(ConfigFactory.parseMap(mapOf("devMode" to true))) .resolve() val fullConfig = nodeConfig.parseAsNodeConfiguration().value() @@ -70,7 +70,7 @@ class NodeConfigTest { .withValue("systemProperties", valueFor(mapOf("property.name" to "value"))) .withValue("custom.jvmArgs", valueFor("-Xmx1000G")) .withValue("baseDirectory", valueFor(baseDir.toString())) - .withFallback(ConfigFactory.parseResources("reference.conf")) + .withFallback(ConfigFactory.parseResources("corda-reference.conf")) .withFallback(ConfigFactory.parseMap(mapOf("devMode" to true))) .resolve() val fullConfig = nodeConfig.parseAsNodeConfiguration().value() diff --git a/tools/network-builder/src/main/kotlin/net/corda/networkbuilder/nodes/NodeBuilder.kt b/tools/network-builder/src/main/kotlin/net/corda/networkbuilder/nodes/NodeBuilder.kt index 6dd3d23359..6c732f8ca9 100644 --- a/tools/network-builder/src/main/kotlin/net/corda/networkbuilder/nodes/NodeBuilder.kt +++ b/tools/network-builder/src/main/kotlin/net/corda/networkbuilder/nodes/NodeBuilder.kt @@ -63,7 +63,7 @@ open class NodeBuilder { fun Config.parseAsNodeConfigWithFallback(preCopyConfig: Config): Validated { val nodeConfig = this .withValue("baseDirectory", ConfigValueFactory.fromAnyRef("")) - .withFallback(ConfigFactory.parseResources("reference.conf")) + .withFallback(ConfigFactory.parseResources("corda-reference.conf")) .withFallback(preCopyConfig) .resolve() return nodeConfig.parseAsNodeConfiguration() From 7ab6a8f60077db505c434a516f49cbea8ec46f9e Mon Sep 17 00:00:00 2001 From: Dan Newton Date: Tue, 16 Jun 2020 09:22:26 +0100 Subject: [PATCH 06/85] CORDA-3841 Check `isAnyCheckpointPersisted` in `startFlowInternal` (#6351) Only hit the database if `StateMachineState.isAnyCheckpointPersisted` returns true. Otherwise, there will be no checkpoint to retrieve from the database anyway. This can prevent errors due to a transient loss of connection to the database. --- .../StatemachineGeneralErrorHandlingTest.kt | 521 ++++++++++++++++-- .../persistence/DBCheckpointStorage.kt | 1 + .../SingleThreadedStateMachineManager.kt | 12 +- .../statemachine/StaffedFlowHospital.kt | 2 + 4 files changed, 489 insertions(+), 47 deletions(-) diff --git a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineGeneralErrorHandlingTest.kt b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineGeneralErrorHandlingTest.kt index 5aacac8a4a..740ea8ca43 100644 --- a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineGeneralErrorHandlingTest.kt +++ b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineGeneralErrorHandlingTest.kt @@ -4,6 +4,7 @@ import net.corda.client.rpc.CordaRPCClient import net.corda.core.messaging.startFlow import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.seconds +import net.corda.node.services.api.CheckpointStorage import net.corda.node.services.messaging.DeduplicationHandler import net.corda.node.services.statemachine.transitions.TopLevelTransition import net.corda.testing.core.ALICE_NAME @@ -11,6 +12,8 @@ import net.corda.testing.core.CHARLIE_NAME import net.corda.testing.core.singleIdentity import org.junit.Ignore import org.junit.Test +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors import java.util.concurrent.TimeoutException import kotlin.test.assertEquals import kotlin.test.assertFailsWith @@ -18,6 +21,10 @@ import kotlin.test.assertFailsWith @Suppress("MaxLineLength") // Byteman rules cannot be easily wrapped class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { + private companion object { + val executor: ExecutorService = Executors.newSingleThreadExecutor() + } + /** * Throws an exception when performing an [Action.SendInitial] action. * The exception is thrown 4 times. @@ -25,8 +32,8 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { * This causes the transition to be discharged from the hospital 3 times (retries 3 times) and is then kept in * the hospital for observation. */ - @Test(timeout=300_000) - fun `error during transition with SendInitial action is retried 3 times and kept for observation if error persists`() { + @Test(timeout = 300_000) + fun `error during transition with SendInitial action is retried 3 times and kept for observation if error persists`() { startDriver { val charlie = createNode(CHARLIE_NAME) val alice = createBytemanNode(ALICE_NAME) @@ -79,7 +86,10 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { CordaRPCClient(alice.rpcAddress).start(rpcUser.username, rpcUser.password).proxy assertFailsWith { - aliceClient.startFlow(StatemachineErrorHandlingTest::SendAMessageFlow, charlie.nodeInfo.singleIdentity()).returnValue.getOrThrow( + aliceClient.startFlow( + StatemachineErrorHandlingTest::SendAMessageFlow, + charlie.nodeInfo.singleIdentity() + ).returnValue.getOrThrow( 30.seconds ) } @@ -105,8 +115,8 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { * This causes the transition to be discharged from the hospital 3 times (retries 3 times). On the final retry the transition * succeeds and the flow finishes. */ - @Test(timeout=300_000) - fun `error during transition with SendInitial action that does not persist will retry and complete successfully`() { + @Test(timeout = 300_000) + fun `error during transition with SendInitial action that does not persist will retry and complete successfully`() { startDriver { val charlie = createNode(CHARLIE_NAME) val alice = createBytemanNode(ALICE_NAME) @@ -158,7 +168,10 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { val aliceClient = CordaRPCClient(alice.rpcAddress).start(rpcUser.username, rpcUser.password).proxy - aliceClient.startFlow(StatemachineErrorHandlingTest::SendAMessageFlow, charlie.nodeInfo.singleIdentity()).returnValue.getOrThrow( + aliceClient.startFlow( + StatemachineErrorHandlingTest::SendAMessageFlow, + charlie.nodeInfo.singleIdentity() + ).returnValue.getOrThrow( 30.seconds ) @@ -185,8 +198,8 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { * The exceptions should be swallowed. Therefore there should be no trips to the hospital and no retries. * The flow should complete successfully as the error is swallowed. */ - @Test(timeout=300_000) - fun `error during transition with AcknowledgeMessages action is swallowed and flow completes successfully`() { + @Test(timeout = 300_000) + fun `error during transition with AcknowledgeMessages action is swallowed and flow completes successfully`() { startDriver { val charlie = createNode(CHARLIE_NAME) val alice = createBytemanNode(ALICE_NAME) @@ -238,7 +251,10 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { val aliceClient = CordaRPCClient(alice.rpcAddress).start(rpcUser.username, rpcUser.password).proxy - aliceClient.startFlow(StatemachineErrorHandlingTest::SendAMessageFlow, charlie.nodeInfo.singleIdentity()).returnValue.getOrThrow( + aliceClient.startFlow( + StatemachineErrorHandlingTest::SendAMessageFlow, + charlie.nodeInfo.singleIdentity() + ).returnValue.getOrThrow( 30.seconds ) @@ -270,8 +286,8 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { * if an error transition moves into another error transition. The flow still recovers from this state. 5 exceptions were thrown to * verify that 3 retries are attempted before recovering. */ - @Test(timeout=300_000) - fun `error during transition with CommitTransaction action that occurs during the beginning of execution will retry and complete successfully`() { + @Test(timeout = 300_000) + fun `error during transition with CommitTransaction action that occurs during the beginning of execution will retry and complete successfully`() { startDriver { val charlie = createNode(CHARLIE_NAME) val alice = createBytemanNode(ALICE_NAME) @@ -323,7 +339,10 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { val aliceClient = CordaRPCClient(alice.rpcAddress).start(rpcUser.username, rpcUser.password).proxy - aliceClient.startFlow(StatemachineErrorHandlingTest::SendAMessageFlow, charlie.nodeInfo.singleIdentity()).returnValue.getOrThrow( + aliceClient.startFlow( + StatemachineErrorHandlingTest::SendAMessageFlow, + charlie.nodeInfo.singleIdentity() + ).returnValue.getOrThrow( 30.seconds ) @@ -356,8 +375,8 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { * * CORDA-3352 - it is currently hanging after putting the flow in for observation */ - @Test(timeout=300_000) -@Ignore + @Test(timeout = 300_000) + @Ignore fun `error during transition with CommitTransaction action that occurs during the beginning of execution will retry and be kept for observation if error persists`() { startDriver { val charlie = createNode(CHARLIE_NAME) @@ -411,7 +430,10 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { CordaRPCClient(alice.rpcAddress).start(rpcUser.username, rpcUser.password).proxy assertFailsWith { - aliceClient.startFlow(StatemachineErrorHandlingTest::SendAMessageFlow, charlie.nodeInfo.singleIdentity()).returnValue.getOrThrow( + aliceClient.startFlow( + StatemachineErrorHandlingTest::SendAMessageFlow, + charlie.nodeInfo.singleIdentity() + ).returnValue.getOrThrow( 30.seconds ) } @@ -443,8 +465,8 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { * if an error transition moves into another error transition. The flow still recovers from this state. 5 exceptions were thrown to * verify that 3 retries are attempted before recovering. */ - @Test(timeout=300_000) - fun `error during transition with CommitTransaction action that occurs after the first suspend will retry and complete successfully`() { + @Test(timeout = 300_000) + fun `error during transition with CommitTransaction action that occurs after the first suspend will retry and complete successfully`() { startDriver { val charlie = createNode(CHARLIE_NAME) val alice = createBytemanNode(ALICE_NAME) @@ -513,7 +535,10 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { val aliceClient = CordaRPCClient(alice.rpcAddress).start(rpcUser.username, rpcUser.password).proxy - aliceClient.startFlow(StatemachineErrorHandlingTest::SendAMessageFlow, charlie.nodeInfo.singleIdentity()).returnValue.getOrThrow( + aliceClient.startFlow( + StatemachineErrorHandlingTest::SendAMessageFlow, + charlie.nodeInfo.singleIdentity() + ).returnValue.getOrThrow( 30.seconds ) @@ -540,8 +565,8 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { * * Each time the flow retries, it begins from the previous checkpoint where it suspended before failing. */ - @Test(timeout=300_000) - fun `error during transition with CommitTransaction action that occurs when completing a flow and deleting its checkpoint will retry and complete successfully`() { + @Test(timeout = 300_000) + fun `error during transition with CommitTransaction action that occurs when completing a flow and deleting its checkpoint will retry and complete successfully`() { startDriver { val charlie = createNode(CHARLIE_NAME) val alice = createBytemanNode(ALICE_NAME) @@ -602,7 +627,10 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { val aliceClient = CordaRPCClient(alice.rpcAddress).start(rpcUser.username, rpcUser.password).proxy - aliceClient.startFlow(StatemachineErrorHandlingTest::SendAMessageFlow, charlie.nodeInfo.singleIdentity()).returnValue.getOrThrow( + aliceClient.startFlow( + StatemachineErrorHandlingTest::SendAMessageFlow, + charlie.nodeInfo.singleIdentity() + ).returnValue.getOrThrow( 30.seconds ) @@ -629,8 +657,8 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { * The flow is discharged and replayed from the hospital once. After failing during the replay, the flow is forced into overnight * observation. It is not ran again after this point */ - @Test(timeout=300_000) - fun `error during retry of a flow will force the flow into overnight observation`() { + @Test(timeout = 300_000) + fun `error during retry of a flow will force the flow into overnight observation`() { startDriver { val charlie = createNode(CHARLIE_NAME) val alice = createBytemanNode(ALICE_NAME) @@ -699,7 +727,10 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { CordaRPCClient(alice.rpcAddress).start(rpcUser.username, rpcUser.password).proxy assertFailsWith { - aliceClient.startFlow(StatemachineErrorHandlingTest::SendAMessageFlow, charlie.nodeInfo.singleIdentity()).returnValue.getOrThrow( + aliceClient.startFlow( + StatemachineErrorHandlingTest::SendAMessageFlow, + charlie.nodeInfo.singleIdentity() + ).returnValue.getOrThrow( 30.seconds ) } @@ -729,8 +760,8 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { * flow will still finish successfully. This is due to the even being scheduled as part of the retry and the failure in the database * commit occurs after this point. As the flow is already scheduled, the failure has not affect on it. */ - @Test(timeout=300_000) - fun `error during commit transaction action when retrying a flow will retry the flow again and complete successfully`() { + @Test(timeout = 300_000) + fun `error during commit transaction action when retrying a flow will retry the flow again and complete successfully`() { startDriver { val charlie = createNode(CHARLIE_NAME) val alice = createBytemanNode(ALICE_NAME) @@ -798,7 +829,10 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { val aliceClient = CordaRPCClient(alice.rpcAddress).start(rpcUser.username, rpcUser.password).proxy - aliceClient.startFlow(StatemachineErrorHandlingTest::SendAMessageFlow, charlie.nodeInfo.singleIdentity()).returnValue.getOrThrow( + aliceClient.startFlow( + StatemachineErrorHandlingTest::SendAMessageFlow, + charlie.nodeInfo.singleIdentity() + ).returnValue.getOrThrow( 30.seconds ) @@ -828,8 +862,8 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { * CORDA-3352 - it is currently hanging after putting the flow in for observation * */ - @Test(timeout=300_000) -@Ignore + @Test(timeout = 300_000) + @Ignore fun `error during retrying a flow that failed when committing its original checkpoint will force the flow into overnight observation`() { startDriver { val charlie = createNode(CHARLIE_NAME) @@ -883,7 +917,10 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { CordaRPCClient(alice.rpcAddress).start(rpcUser.username, rpcUser.password).proxy assertFailsWith { - aliceClient.startFlow(StatemachineErrorHandlingTest::SendAMessageFlow, charlie.nodeInfo.singleIdentity()).returnValue.getOrThrow( + aliceClient.startFlow( + StatemachineErrorHandlingTest::SendAMessageFlow, + charlie.nodeInfo.singleIdentity() + ).returnValue.getOrThrow( 30.seconds ) } @@ -910,8 +947,8 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { * * Each time the flow retries, it begins from the previous checkpoint where it suspended before failing. */ - @Test(timeout=300_000) - fun `error during transition with CommitTransaction action and ConstraintViolationException that occurs when completing a flow will retry and be kept for observation if error persists`() { + @Test(timeout = 300_000) + fun `error during transition with CommitTransaction action and ConstraintViolationException that occurs when completing a flow will retry and be kept for observation if error persists`() { startDriver { val charlie = createNode(CHARLIE_NAME) val alice = createBytemanNode(ALICE_NAME) @@ -975,7 +1012,10 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { CordaRPCClient(alice.rpcAddress).start(rpcUser.username, rpcUser.password).proxy assertFailsWith { - aliceClient.startFlow(StatemachineErrorHandlingTest::SendAMessageFlow, charlie.nodeInfo.singleIdentity()).returnValue.getOrThrow( + aliceClient.startFlow( + StatemachineErrorHandlingTest::SendAMessageFlow, + charlie.nodeInfo.singleIdentity() + ).returnValue.getOrThrow( 30.seconds ) } @@ -994,6 +1034,196 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { } } + /** + * Throws an exception when performing an [Action.CommitTransaction] event before the flow has suspended (remains in an unstarted + * state). + * + * The exception is thrown 5 times. + * + * An exception is also thrown from [CheckpointStorage.getCheckpoint]. + * + * This test is to prevent a regression, where a transient database connection error can be thrown retrieving a flow's checkpoint when + * retrying the flow after it failed to commit it's original checkpoint. + * + * This causes the transition to be discharged from the hospital 3 times (retries 3 times). On the final retry the transition + * succeeds and the flow finishes. + */ + @Test(timeout = 300_000) + fun `flow can be retried when there is a transient connection error to the database`() { + startDriver { + val charlie = createNode(CHARLIE_NAME) + val alice = createBytemanNode(ALICE_NAME) + + val rules = """ + RULE Create Counter + CLASS ${ActionExecutorImpl::class.java.name} + METHOD executeCommitTransaction + AT ENTRY + IF createCounter("counter", $counter) + DO traceln("Counter created") + ENDRULE + + RULE Throw exception on executeCommitTransaction action + CLASS ${ActionExecutorImpl::class.java.name} + METHOD executeCommitTransaction + AT ENTRY + IF readCounter("counter") < 5 + DO incrementCounter("counter"); traceln("Throwing exception"); throw new java.lang.RuntimeException("die dammit die") + ENDRULE + + RULE Throw exception on getCheckpoint + INTERFACE ${CheckpointStorage::class.java.name} + METHOD getCheckpoint + AT ENTRY + IF true + DO traceln("Throwing exception getting checkpoint"); throw new java.sql.SQLTransientConnectionException("Connection is not available") + ENDRULE + + RULE Entering internal error staff member + CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} + METHOD consult + AT ENTRY + IF true + DO traceln("Reached internal transition error staff member") + ENDRULE + + RULE Increment discharge counter + CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} + METHOD consult + AT READ DISCHARGE + IF true + DO traceln("Byteman test - discharging") + ENDRULE + + RULE Increment observation counter + CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} + METHOD consult + AT READ OVERNIGHT_OBSERVATION + IF true + DO traceln("Byteman test - overnight observation") + ENDRULE + """.trimIndent() + + submitBytemanRules(rules) + + val aliceClient = + CordaRPCClient(alice.rpcAddress).start(rpcUser.username, rpcUser.password).proxy + + aliceClient.startFlow( + StatemachineErrorHandlingTest::SendAMessageFlow, + charlie.nodeInfo.singleIdentity() + ).returnValue.getOrThrow( + 30.seconds + ) + + val output = getBytemanOutput(alice) + + // Check the stdout for the lines generated by byteman + assertEquals(3, output.filter { it.contains("Byteman test - discharging") }.size) + assertEquals(0, output.filter { it.contains("Byteman test - overnight observation") }.size) + val (discharge, observation) = aliceClient.startFlow(StatemachineErrorHandlingTest::GetHospitalCountersFlow).returnValue.get() + assertEquals(3, discharge) + assertEquals(0, observation) + assertEquals(0, aliceClient.stateMachinesSnapshot().size) + assertEquals(1, aliceClient.startFlow(StatemachineErrorHandlingTest::GetNumberOfCheckpointsFlow).returnValue.get()) + } + } + + /** + * Throws an exception when performing an [Action.CommitTransaction] event before the flow has suspended (remains in an unstarted + * state). + * + * The exception is thrown 7 times. + * + * An exception is also thrown from [CheckpointStorage.getCheckpoint]. + * + * This test is to prevent a regression, where a transient database connection error can be thrown retrieving a flow's checkpoint when + * retrying the flow after it failed to commit it's original checkpoint. + * + * This causes the transition to be discharged from the hospital 3 times (retries 3 times). On the final retry the transition + * fails and is kept for in for observation. + */ + @Test(timeout = 300_000) + fun `flow can be retried when there is a transient connection error to the database goes to observation if error persists`() { + startDriver { + val charlie = createNode(CHARLIE_NAME) + val alice = createBytemanNode(ALICE_NAME) + + val rules = """ + RULE Create Counter + CLASS ${ActionExecutorImpl::class.java.name} + METHOD executeCommitTransaction + AT ENTRY + IF createCounter("counter", $counter) + DO traceln("Counter created") + ENDRULE + + RULE Throw exception on executeCommitTransaction action + CLASS ${ActionExecutorImpl::class.java.name} + METHOD executeCommitTransaction + AT ENTRY + IF readCounter("counter") < 7 + DO incrementCounter("counter"); traceln("Throwing exception"); throw new java.lang.RuntimeException("die dammit die") + ENDRULE + + RULE Throw exception on getCheckpoint + INTERFACE ${CheckpointStorage::class.java.name} + METHOD getCheckpoint + AT ENTRY + IF true + DO traceln("Throwing exception getting checkpoint"); throw new java.sql.SQLTransientConnectionException("Connection is not available") + ENDRULE + + RULE Entering internal error staff member + CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} + METHOD consult + AT ENTRY + IF true + DO traceln("Reached internal transition error staff member") + ENDRULE + + RULE Increment discharge counter + CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} + METHOD consult + AT READ DISCHARGE + IF true + DO traceln("Byteman test - discharging") + ENDRULE + + RULE Increment observation counter + CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} + METHOD consult + AT READ OVERNIGHT_OBSERVATION + IF true + DO traceln("Byteman test - overnight observation") + ENDRULE + """.trimIndent() + + submitBytemanRules(rules) + + val aliceClient = + CordaRPCClient(alice.rpcAddress).start(rpcUser.username, rpcUser.password).proxy + + executor.execute { + aliceClient.startFlow(StatemachineErrorHandlingTest::SendAMessageFlow, charlie.nodeInfo.singleIdentity()) + } + + // flow is not signaled as started calls to [getOrThrow] will hang, sleeping instead + Thread.sleep(30.seconds.toMillis()) + + val output = getBytemanOutput(alice) + + // Check the stdout for the lines generated by byteman + assertEquals(3, output.filter { it.contains("Byteman test - discharging") }.size) + assertEquals(1, output.filter { it.contains("Byteman test - overnight observation") }.size) + val (discharge, observation) = aliceClient.startFlow(StatemachineErrorHandlingTest::GetHospitalCountersFlow).returnValue.get() + assertEquals(3, discharge) + assertEquals(1, observation) + assertEquals(1, aliceClient.stateMachinesSnapshot().size) + assertEquals(1, aliceClient.startFlow(StatemachineErrorHandlingTest::GetNumberOfCheckpointsFlow).returnValue.get()) + } + } + /** * Throws an exception when performing an [Action.CommitTransaction] event on a responding flow. The failure prevents the node from saving * its original checkpoint. @@ -1009,8 +1239,8 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { * if an error transition moves into another error transition. The flow still recovers from this state. 5 exceptions were thrown to verify * that 3 retries are attempted before recovering. */ - @Test(timeout=300_000) - fun `responding flow - error during transition with CommitTransaction action that occurs during the beginning of execution will retry and complete successfully`() { + @Test(timeout = 300_000) + fun `responding flow - error during transition with CommitTransaction action that occurs during the beginning of execution will retry and complete successfully`() { startDriver { val charlie = createBytemanNode(CHARLIE_NAME) val alice = createNode(ALICE_NAME) @@ -1064,7 +1294,10 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { val charlieClient = CordaRPCClient(charlie.rpcAddress).start(rpcUser.username, rpcUser.password).proxy - aliceClient.startFlow(StatemachineErrorHandlingTest::SendAMessageFlow, charlie.nodeInfo.singleIdentity()).returnValue.getOrThrow( + aliceClient.startFlow( + StatemachineErrorHandlingTest::SendAMessageFlow, + charlie.nodeInfo.singleIdentity() + ).returnValue.getOrThrow( 30.seconds ) @@ -1104,8 +1337,8 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { * able to recover when the node is restarted (by using the events). The initiating flow maintains the checkpoint as it is waiting for * the responding flow to recover and finish its flow. */ - @Test(timeout=300_000) - fun `responding flow - error during transition with CommitTransaction action that occurs during the beginning of execution will retry and be kept for observation if error persists`() { + @Test(timeout = 300_000) + fun `responding flow - error during transition with CommitTransaction action that occurs during the beginning of execution will retry and be kept for observation if error persists`() { startDriver { val charlie = createBytemanNode(CHARLIE_NAME) val alice = createNode(ALICE_NAME) @@ -1160,7 +1393,10 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { CordaRPCClient(charlie.rpcAddress).start(rpcUser.username, rpcUser.password).proxy assertFailsWith { - aliceClient.startFlow(StatemachineErrorHandlingTest::SendAMessageFlow, charlie.nodeInfo.singleIdentity()).returnValue.getOrThrow( + aliceClient.startFlow( + StatemachineErrorHandlingTest::SendAMessageFlow, + charlie.nodeInfo.singleIdentity() + ).returnValue.getOrThrow( 30.seconds ) } @@ -1192,8 +1428,8 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { * This causes the transition to be discharged from the hospital 3 times (retries 3 times). On the final retry the transition * succeeds and the flow finishes. */ - @Test(timeout=300_000) - fun `responding flow - error during transition with CommitTransaction action that occurs when completing a flow and deleting its checkpoint will retry and complete successfully`() { + @Test(timeout = 300_000) + fun `responding flow - error during transition with CommitTransaction action that occurs when completing a flow and deleting its checkpoint will retry and complete successfully`() { startDriver { val charlie = createBytemanNode(CHARLIE_NAME) val alice = createNode(ALICE_NAME) @@ -1258,7 +1494,10 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { val charlieClient = CordaRPCClient(charlie.rpcAddress).start(rpcUser.username, rpcUser.password).proxy - aliceClient.startFlow(StatemachineErrorHandlingTest::SendAMessageFlow, charlie.nodeInfo.singleIdentity()).returnValue.getOrThrow( + aliceClient.startFlow( + StatemachineErrorHandlingTest::SendAMessageFlow, + charlie.nodeInfo.singleIdentity() + ).returnValue.getOrThrow( 30.seconds ) @@ -1278,4 +1517,202 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { assertEquals(1, charlieClient.startFlow(StatemachineErrorHandlingTest::GetNumberOfCheckpointsFlow).returnValue.get()) } } + + /** + * Throws an exception when performing an [Action.CommitTransaction] event before the flow has suspended (remains in an unstarted + * state) on a responding node. + * + * The exception is thrown 5 times. + * + * An exception is also thrown from [CheckpointStorage.getCheckpoint]. + * + * This test is to prevent a regression, where a transient database connection error can be thrown retrieving a flow's checkpoint when + * retrying the flow after it failed to commit it's original checkpoint. + * + * This causes the transition to be discharged from the hospital 3 times (retries 3 times). On the final retry the transition + * succeeds and the flow finishes. + */ + @Test(timeout = 300_000) + fun `responding flow - session init can be retried when there is a transient connection error to the database`() { + startDriver { + val charlie = createBytemanNode(CHARLIE_NAME) + val alice = createNode(ALICE_NAME) + + val rules = """ + RULE Create Counter + CLASS ${ActionExecutorImpl::class.java.name} + METHOD executeCommitTransaction + AT ENTRY + IF createCounter("counter", $counter) + DO traceln("Counter created") + ENDRULE + + RULE Throw exception on executeCommitTransaction action + CLASS ${ActionExecutorImpl::class.java.name} + METHOD executeCommitTransaction + AT ENTRY + IF readCounter("counter") < 5 + DO incrementCounter("counter"); traceln("Throwing exception"); throw new java.lang.RuntimeException("die dammit die") + ENDRULE + + RULE Throw exception on getCheckpoint + INTERFACE ${CheckpointStorage::class.java.name} + METHOD getCheckpoint + AT ENTRY + IF true + DO traceln("Throwing exception getting checkpoint"); throw new java.sql.SQLTransientConnectionException("Connection is not available") + ENDRULE + + RULE Entering internal error staff member + CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} + METHOD consult + AT ENTRY + IF true + DO traceln("Reached internal transition error staff member") + ENDRULE + + RULE Increment discharge counter + CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} + METHOD consult + AT READ DISCHARGE + IF true + DO traceln("Byteman test - discharging") + ENDRULE + + RULE Increment observation counter + CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} + METHOD consult + AT READ OVERNIGHT_OBSERVATION + IF true + DO traceln("Byteman test - overnight observation") + ENDRULE + """.trimIndent() + + submitBytemanRules(rules) + + val aliceClient = + CordaRPCClient(alice.rpcAddress).start(rpcUser.username, rpcUser.password).proxy + val charlieClient = + CordaRPCClient(charlie.rpcAddress).start(rpcUser.username, rpcUser.password).proxy + + aliceClient.startFlow( + StatemachineErrorHandlingTest::SendAMessageFlow, + charlie.nodeInfo.singleIdentity() + ).returnValue.getOrThrow( + 30.seconds + ) + + val output = getBytemanOutput(charlie) + + // Check the stdout for the lines generated by byteman + assertEquals(3, output.filter { it.contains("Byteman test - discharging") }.size) + assertEquals(0, output.filter { it.contains("Byteman test - overnight observation") }.size) + val (discharge, observation) = charlieClient.startFlow(StatemachineErrorHandlingTest::GetHospitalCountersFlow).returnValue.get() + assertEquals(3, discharge) + assertEquals(0, observation) + assertEquals(0, aliceClient.stateMachinesSnapshot().size) + assertEquals(0, charlieClient.stateMachinesSnapshot().size) + assertEquals(1, charlieClient.startFlow(StatemachineErrorHandlingTest::GetNumberOfCheckpointsFlow).returnValue.get()) + } + } + + /** + * Throws an exception when performing an [Action.CommitTransaction] event before the flow has suspended (remains in an unstarted + * state) on a responding node. + * + * The exception is thrown 7 times. + * + * An exception is also thrown from [CheckpointStorage.getCheckpoint]. + * + * This test is to prevent a regression, where a transient database connection error can be thrown retrieving a flow's checkpoint when + * retrying the flow after it failed to commit it's original checkpoint. + * + * This causes the transition to be discharged from the hospital 3 times (retries 3 times). On the final retry the transition + * fails and is kept for in for observation. + */ + @Test(timeout = 300_000) + fun `responding flow - session init can be retried when there is a transient connection error to the database goes to observation if error persists`() { + startDriver { + val charlie = createBytemanNode(CHARLIE_NAME) + val alice = createNode(ALICE_NAME) + + val rules = """ + RULE Create Counter + CLASS ${ActionExecutorImpl::class.java.name} + METHOD executeCommitTransaction + AT ENTRY + IF createCounter("counter", $counter) + DO traceln("Counter created") + ENDRULE + + RULE Throw exception on executeCommitTransaction action + CLASS ${ActionExecutorImpl::class.java.name} + METHOD executeCommitTransaction + AT ENTRY + IF readCounter("counter") < 7 + DO incrementCounter("counter"); traceln("Throwing exception"); throw new java.lang.RuntimeException("die dammit die") + ENDRULE + + RULE Throw exception on getCheckpoint + INTERFACE ${CheckpointStorage::class.java.name} + METHOD getCheckpoint + AT ENTRY + IF true + DO traceln("Throwing exception getting checkpoint"); throw new java.sql.SQLTransientConnectionException("Connection is not available") + ENDRULE + + RULE Entering internal error staff member + CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} + METHOD consult + AT ENTRY + IF true + DO traceln("Reached internal transition error staff member") + ENDRULE + + RULE Increment discharge counter + CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} + METHOD consult + AT READ DISCHARGE + IF true + DO traceln("Byteman test - discharging") + ENDRULE + + RULE Increment observation counter + CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} + METHOD consult + AT READ OVERNIGHT_OBSERVATION + IF true + DO traceln("Byteman test - overnight observation") + ENDRULE + """.trimIndent() + + submitBytemanRules(rules) + + val aliceClient = + CordaRPCClient(alice.rpcAddress).start(rpcUser.username, rpcUser.password).proxy + val charlieClient = + CordaRPCClient(charlie.rpcAddress).start(rpcUser.username, rpcUser.password).proxy + + assertFailsWith { + aliceClient.startFlow( + StatemachineErrorHandlingTest::SendAMessageFlow, + charlie.nodeInfo.singleIdentity() + ).returnValue.getOrThrow( + 30.seconds + ) + } + + val output = getBytemanOutput(charlie) + + // Check the stdout for the lines generated by byteman + assertEquals(3, output.filter { it.contains("Byteman test - discharging") }.size) + assertEquals(1, output.filter { it.contains("Byteman test - overnight observation") }.size) + val (discharge, observation) = charlieClient.startFlow(StatemachineErrorHandlingTest::GetHospitalCountersFlow).returnValue.get() + assertEquals(3, discharge) + assertEquals(1, observation) + assertEquals(1, aliceClient.stateMachinesSnapshot().size) + assertEquals(1, charlieClient.stateMachinesSnapshot().size) + assertEquals(1, charlieClient.startFlow(StatemachineErrorHandlingTest::GetNumberOfCheckpointsFlow).returnValue.get()) + } + } } \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBCheckpointStorage.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBCheckpointStorage.kt index b1eec763f6..78a0e627bd 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBCheckpointStorage.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBCheckpointStorage.kt @@ -66,6 +66,7 @@ class DBCheckpointStorage : CheckpointStorage { return session.createQuery(delete).executeUpdate() > 0 } + @Throws(SQLException::class) override fun getCheckpoint(id: StateMachineRunId): SerializedBytes? { val bytes = currentDBSession().get(DBCheckpoint::class.java, id.uuid.toString())?.checkpoint ?: return null return SerializedBytes(bytes) diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/SingleThreadedStateMachineManager.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/SingleThreadedStateMachineManager.kt index 595b644493..d6e58f84b9 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/SingleThreadedStateMachineManager.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/SingleThreadedStateMachineManager.kt @@ -607,9 +607,8 @@ class SingleThreadedStateMachineManager( val flowCorDappVersion = createSubFlowVersion(serviceHub.cordappProvider.getCordappForFlow(flowLogic), serviceHub.myInfo.platformVersion) - val flowAlreadyExists = mutex.locked { flows[flowId] != null } - - val existingCheckpoint = if (flowAlreadyExists) { + val existingFlow = mutex.locked { flows[flowId] } + val existingCheckpoint = if (existingFlow != null && existingFlow.fiber.transientState?.value?.isAnyCheckpointPersisted == true) { // Load the flow's checkpoint // The checkpoint will be missing if the flow failed before persisting the original checkpoint // CORDA-3359 - Do not start/retry a flow that failed after deleting its checkpoint (the whole of the flow might replay) @@ -617,8 +616,10 @@ class SingleThreadedStateMachineManager( val checkpoint = tryCheckpointDeserialize(serializedCheckpoint, flowId) if (checkpoint == null) { return openFuture>().mapError { - IllegalStateException("Unable to deserialize database checkpoint for flow $flowId. " + - "Something is very wrong. The flow will not retry.") + IllegalStateException( + "Unable to deserialize database checkpoint for flow $flowId. " + + "Something is very wrong. The flow will not retry." + ) } } else { checkpoint @@ -628,6 +629,7 @@ class SingleThreadedStateMachineManager( // This is a brand new flow null } + val checkpoint = existingCheckpoint ?: Checkpoint.create( invocationContext, flowStart, diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/StaffedFlowHospital.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/StaffedFlowHospital.kt index 6408748b42..065b352ff6 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/StaffedFlowHospital.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/StaffedFlowHospital.kt @@ -146,6 +146,8 @@ class StaffedFlowHospital(private val flowMessaging: FlowMessaging, val payload = RejectSessionMessage(message, secureRandom.nextLong()) val replyError = ExistingSessionMessage(sessionMessage.initiatorSessionId, payload) + log.info("Sending session initiation error back to $sender", error) + flowMessaging.sendSessionMessage(sender, replyError, SenderDeduplicationId(DeduplicationId.createRandom(secureRandom), ourSenderUUID)) event.deduplicationHandler.afterDatabaseTransaction() } From 24b0240d822d341dc70b31e6d29c857d9ce7b56d Mon Sep 17 00:00:00 2001 From: James Higgs <45565019+JamesHR3@users.noreply.github.com> Date: Wed, 17 Jun 2020 14:32:12 +0100 Subject: [PATCH 07/85] EG-2654 - Ensure stack traces are printed to the logs in error reporting (#6345) * EG-2654 Ensure stack trace is printed to the logs in error reporting * EG-2654 - Add a test case for exception logging --- .../errorReporting/ErrorReporterImpl.kt | 7 ++++++- .../database-failed-startup.properties | 2 +- .../database-failed-startup_en_US.properties | 2 +- .../errorReporting/DatabaseErrorsTest.kt | 2 +- .../errorReporting/ErrorReporterImplTest.kt | 18 +++++++++++++++++- .../errorReporting/test-case4.properties | 4 ++++ .../errorReporting/test-case4_en_US.properties | 4 ++++ .../net/corda/node/internal/AbstractNode.kt | 7 +++++-- 8 files changed, 39 insertions(+), 7 deletions(-) create mode 100644 common/logging/src/test/resources/errorReporting/test-case4.properties create mode 100644 common/logging/src/test/resources/errorReporting/test-case4_en_US.properties diff --git a/common/logging/src/main/kotlin/net/corda/common/logging/errorReporting/ErrorReporterImpl.kt b/common/logging/src/main/kotlin/net/corda/common/logging/errorReporting/ErrorReporterImpl.kt index 0e508959e8..553303870e 100644 --- a/common/logging/src/main/kotlin/net/corda/common/logging/errorReporting/ErrorReporterImpl.kt +++ b/common/logging/src/main/kotlin/net/corda/common/logging/errorReporting/ErrorReporterImpl.kt @@ -1,6 +1,7 @@ package net.corda.common.logging.errorReporting import org.slf4j.Logger +import java.lang.Exception import java.text.MessageFormat import java.util.* @@ -31,6 +32,10 @@ internal class ErrorReporterImpl(private val resourceLocation: String, override fun report(error: ErrorCode<*>, logger: Logger) { val errorResource = ErrorResource.fromErrorCode(error, resourceLocation, locale) val message = "${errorResource.getErrorMessage(error.parameters.toTypedArray())} ${getErrorInfo(error)}" - logger.error(message) + if (error is Exception) { + logger.error(message, error) + } else { + logger.error(message) + } } } \ No newline at end of file diff --git a/common/logging/src/main/resources/error-codes/database-failed-startup.properties b/common/logging/src/main/resources/error-codes/database-failed-startup.properties index 996de3ba76..2c21cd3e9a 100644 --- a/common/logging/src/main/resources/error-codes/database-failed-startup.properties +++ b/common/logging/src/main/resources/error-codes/database-failed-startup.properties @@ -1,4 +1,4 @@ -errorTemplate = Failed to create the datasource. See the logs for further information and the cause. +errorTemplate = Failed to create the datasource: {0}. See the logs for further information and the cause. shortDescription = The datasource could not be created for unknown reasons. actionsToFix = The logs in the logs directory should contain more information on what went wrong. aliases = \ No newline at end of file diff --git a/common/logging/src/main/resources/error-codes/database-failed-startup_en_US.properties b/common/logging/src/main/resources/error-codes/database-failed-startup_en_US.properties index 1abe8840bb..194292abf5 100644 --- a/common/logging/src/main/resources/error-codes/database-failed-startup_en_US.properties +++ b/common/logging/src/main/resources/error-codes/database-failed-startup_en_US.properties @@ -1,3 +1,3 @@ -errorTemplate = Failed to create the datasource. See the logs for further information and the cause. +errorTemplate = Failed to create the datasource: {0}. See the logs for further information and the cause. shortDescription = The datasource could not be created for unknown reasons. actionsToFix = The logs in the logs directory should contain more information on what went wrong. \ No newline at end of file diff --git a/common/logging/src/test/kotlin/net/corda/commmon/logging/errorReporting/DatabaseErrorsTest.kt b/common/logging/src/test/kotlin/net/corda/commmon/logging/errorReporting/DatabaseErrorsTest.kt index d8697e9415..753325a0cd 100644 --- a/common/logging/src/test/kotlin/net/corda/commmon/logging/errorReporting/DatabaseErrorsTest.kt +++ b/common/logging/src/test/kotlin/net/corda/commmon/logging/errorReporting/DatabaseErrorsTest.kt @@ -6,7 +6,7 @@ import java.net.InetAddress class DatabaseErrorsTest : ErrorCodeTest(NodeDatabaseErrors::class.java) { override val dataForCodes = mapOf( NodeDatabaseErrors.COULD_NOT_CONNECT to listOf(), - NodeDatabaseErrors.FAILED_STARTUP to listOf(), + NodeDatabaseErrors.FAILED_STARTUP to listOf("This is a test message"), NodeDatabaseErrors.MISSING_DRIVER to listOf(), NodeDatabaseErrors.PASSWORD_REQUIRED_FOR_H2 to listOf(InetAddress.getLocalHost()) ) diff --git a/common/logging/src/test/kotlin/net/corda/commmon/logging/errorReporting/ErrorReporterImplTest.kt b/common/logging/src/test/kotlin/net/corda/commmon/logging/errorReporting/ErrorReporterImplTest.kt index 40efb4e164..95f9d38141 100644 --- a/common/logging/src/test/kotlin/net/corda/commmon/logging/errorReporting/ErrorReporterImplTest.kt +++ b/common/logging/src/test/kotlin/net/corda/commmon/logging/errorReporting/ErrorReporterImplTest.kt @@ -7,6 +7,7 @@ import net.corda.common.logging.errorReporting.ErrorContextProvider import net.corda.common.logging.errorReporting.ErrorReporterImpl import org.junit.After import org.junit.Test +import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.anyString import org.mockito.Mockito import org.slf4j.Logger @@ -24,6 +25,7 @@ class ErrorReporterImplTest { private val loggerMock = Mockito.mock(Logger::class.java).also { Mockito.`when`(it.error(anyString())).then { logs.addAll(it.arguments) } + Mockito.`when`(it.error(anyString(), any(Exception::class.java))).then { params -> logs.addAll(params.arguments) } } private val contextProvider: ErrorContextProvider = object : ErrorContextProvider { @@ -39,7 +41,8 @@ class ErrorReporterImplTest { private enum class TestErrors : ErrorCodes { CASE1, CASE2, - CASE_3; + CASE_3, + CASE4; override val namespace = TestNamespaces.TEST.toString() } @@ -59,6 +62,11 @@ class ErrorReporterImplTest { override val parameters = listOf() } + private class TestError4(cause: Exception?) : Exception("This is test error 4", cause), ErrorCode { + override val code = TestErrors.CASE4 + override val parameters = listOf() + } + private fun createReporterImpl(localeTag: String?) : ErrorReporterImpl { val locale = if (localeTag != null) Locale.forLanguageTag(localeTag) else Locale.getDefault() return ErrorReporterImpl("errorReporting", locale, contextProvider) @@ -118,4 +126,12 @@ class ErrorReporterImplTest { testReporter.report(error, loggerMock) assertEquals(listOf("This is the third test message [Code: test-case-3 URL: $TEST_URL/en-US]"), logs) } + + @Test(timeout = 3_000) + fun `exception based error code logs the stack trace`() { + val error = TestError4(Exception("A test exception")) + val testReporter = createReporterImpl("en-US") + testReporter.report(error, loggerMock) + assertEquals(listOf("This is the fourth test message [Code: test-case4 URL: $TEST_URL/en-US]", error), logs) + } } \ No newline at end of file diff --git a/common/logging/src/test/resources/errorReporting/test-case4.properties b/common/logging/src/test/resources/errorReporting/test-case4.properties new file mode 100644 index 0000000000..e4911daacf --- /dev/null +++ b/common/logging/src/test/resources/errorReporting/test-case4.properties @@ -0,0 +1,4 @@ +errorTemplate = This is the fourth test message +shortDescription = Test description +actionsToFix = Actions +aliases = \ No newline at end of file diff --git a/common/logging/src/test/resources/errorReporting/test-case4_en_US.properties b/common/logging/src/test/resources/errorReporting/test-case4_en_US.properties new file mode 100644 index 0000000000..e4911daacf --- /dev/null +++ b/common/logging/src/test/resources/errorReporting/test-case4_en_US.properties @@ -0,0 +1,4 @@ +errorTemplate = This is the fourth test message +shortDescription = Test description +actionsToFix = Actions +aliases = \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 9c015067e3..e53e6bd418 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -1360,11 +1360,14 @@ fun CordaPersistence.startHikariPool(hikariProperties: Properties, databaseConfi "Could not find the database driver class. Please add it to the 'drivers' folder.", NodeDatabaseErrors.MISSING_DRIVER) ex is OutstandingDatabaseChangesException -> throw (DatabaseIncompatibleException(ex.message)) - else -> + else -> { + val msg = ex.message ?: ex::class.java.canonicalName throw CouldNotCreateDataSourceException( "Could not create the DataSource: ${ex.message}", NodeDatabaseErrors.FAILED_STARTUP, - cause = ex) + cause = ex, + parameters = listOf(msg)) + } } } } From 0f1bfb13dab98af32cd803c3c40c988906e32ecd Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Wed, 17 Jun 2020 16:09:03 +0100 Subject: [PATCH 08/85] CORDA-3831: Prevent CordappImpl TEST_INSTANCE crashing node when PWD is file-system root directory. (#6360) --- .../kotlin/net/corda/core/internal/cordapp/CordappImpl.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt b/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt index 35e4d0a65d..d511ba7860 100644 --- a/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt +++ b/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt @@ -47,7 +47,7 @@ data class CordappImpl( } companion object { - fun jarName(url: URL): String = url.toPath().fileName.toString().removeSuffix(".jar") + fun jarName(url: URL): String = (url.toPath().fileName ?: "").toString().removeSuffix(".jar") /** CorDapp manifest entries */ const val CORDAPP_CONTRACT_NAME = "Cordapp-Contract-Name" @@ -81,7 +81,7 @@ data class CordappImpl( serializationCustomSerializers = emptyList(), customSchemas = emptySet(), jarPath = Paths.get("").toUri().toURL(), - info = CordappImpl.UNKNOWN_INFO, + info = UNKNOWN_INFO, allFlows = emptyList(), jarHash = SecureHash.allOnesHash, minimumPlatformVersion = 1, From d1482fb6a82c8f62ba95eb38e753952abe4d83f4 Mon Sep 17 00:00:00 2001 From: Walter Oggioni <6357328+woggioni@users.noreply.github.com> Date: Wed, 17 Jun 2020 17:13:30 +0100 Subject: [PATCH 09/85] removed duplicate gradle include (#6330) subproject `tools:checkpoint-agent` is already included at line 63 --- settings.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/settings.gradle b/settings.gradle index ae6dad0838..a32e4d7dee 100644 --- a/settings.gradle +++ b/settings.gradle @@ -104,7 +104,6 @@ include 'core-deterministic:testing:data' include 'core-deterministic:testing:verifier' include 'serialization-deterministic' -include 'tools:checkpoint-agent' include 'detekt-plugins' include 'tools:error-tool' From d0c0a1d9ba5754ae9bb8777a8a821d267696b876 Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Wed, 17 Jun 2020 17:28:26 +0100 Subject: [PATCH 10/85] ENT-5430: Fix deserialisation of commands containing generic types. (#6359) --- .../serialization/generics/DataObject.kt | 14 +++++ .../generics/GenericTypeContract.kt | 37 ++++++++++++ .../serialization/generics/GenericTypeFlow.kt | 27 +++++++++ .../corda/node/ContractWithGenericTypeTest.kt | 52 ++++++++++++++++ ...eterministicContractWithGenericTypeTest.kt | 59 +++++++++++++++++++ .../djvm/SandboxSerializerFactoryFactory.kt | 2 +- .../internal/amqp/LocalSerializerFactory.kt | 3 +- .../internal/amqp/SerializerFactoryBuilder.kt | 2 +- .../internal/model/TypeIdentifier.kt | 18 +++--- .../model/TypeModellingFingerPrinter.kt | 12 ++-- .../amqp/TypeModellingFingerPrinterTests.kt | 2 +- 11 files changed, 207 insertions(+), 21 deletions(-) create mode 100644 node/src/integration-test/kotlin/net/corda/contracts/serialization/generics/DataObject.kt create mode 100644 node/src/integration-test/kotlin/net/corda/contracts/serialization/generics/GenericTypeContract.kt create mode 100644 node/src/integration-test/kotlin/net/corda/flows/serialization/generics/GenericTypeFlow.kt create mode 100644 node/src/integration-test/kotlin/net/corda/node/ContractWithGenericTypeTest.kt create mode 100644 node/src/integration-test/kotlin/net/corda/node/services/DeterministicContractWithGenericTypeTest.kt diff --git a/node/src/integration-test/kotlin/net/corda/contracts/serialization/generics/DataObject.kt b/node/src/integration-test/kotlin/net/corda/contracts/serialization/generics/DataObject.kt new file mode 100644 index 0000000000..6384bd3900 --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/contracts/serialization/generics/DataObject.kt @@ -0,0 +1,14 @@ +package net.corda.contracts.serialization.generics + +import net.corda.core.serialization.CordaSerializable + +@CordaSerializable +data class DataObject(val value: Long) : Comparable { + override fun toString(): String { + return "$value data points" + } + + override fun compareTo(other: DataObject): Int { + return value.compareTo(other.value) + } +} diff --git a/node/src/integration-test/kotlin/net/corda/contracts/serialization/generics/GenericTypeContract.kt b/node/src/integration-test/kotlin/net/corda/contracts/serialization/generics/GenericTypeContract.kt new file mode 100644 index 0000000000..4fcdae9da3 --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/contracts/serialization/generics/GenericTypeContract.kt @@ -0,0 +1,37 @@ +package net.corda.contracts.serialization.generics + +import net.corda.core.contracts.CommandData +import net.corda.core.contracts.Contract +import net.corda.core.contracts.ContractState +import net.corda.core.identity.AbstractParty +import net.corda.core.transactions.LedgerTransaction +import java.util.Optional + +@Suppress("unused") +class GenericTypeContract : Contract { + override fun verify(tx: LedgerTransaction) { + val state = tx.outputsOfType() + require(state.isNotEmpty()) { + "Requires at least one data state" + } + } + + @Suppress("CanBeParameter", "MemberVisibilityCanBePrivate") + class State(val owner: AbstractParty, val data: DataObject) : ContractState { + override val participants: List = listOf(owner) + + @Override + override fun toString(): String { + return data.toString() + } + } + + /** + * The [price] field is the important feature of the [Purchase] + * class because its type is [Optional] with a CorDapp-specific + * generic type parameter. It does not matter that the [price] + * is not used; it only matters that the [Purchase] command + * must be serialized as part of building a new transaction. + */ + class Purchase(val price: Optional) : CommandData +} \ No newline at end of file diff --git a/node/src/integration-test/kotlin/net/corda/flows/serialization/generics/GenericTypeFlow.kt b/node/src/integration-test/kotlin/net/corda/flows/serialization/generics/GenericTypeFlow.kt new file mode 100644 index 0000000000..2325d767b0 --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/flows/serialization/generics/GenericTypeFlow.kt @@ -0,0 +1,27 @@ +package net.corda.flows.serialization.generics + +import co.paralleluniverse.fibers.Suspendable +import net.corda.contracts.serialization.generics.DataObject +import net.corda.contracts.serialization.generics.GenericTypeContract.Purchase +import net.corda.contracts.serialization.generics.GenericTypeContract.State +import net.corda.core.contracts.Command +import net.corda.core.crypto.SecureHash +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.StartableByRPC +import net.corda.core.transactions.TransactionBuilder +import java.util.Optional + +@StartableByRPC +class GenericTypeFlow(private val purchase: DataObject) : FlowLogic() { + @Suspendable + override fun call(): SecureHash { + val notary = serviceHub.networkMapCache.notaryIdentities[0] + val stx = serviceHub.signInitialTransaction( + TransactionBuilder(notary) + .addOutputState(State(ourIdentity, purchase)) + .addCommand(Command(Purchase(Optional.of(purchase)), ourIdentity.owningKey)) + ) + stx.verify(serviceHub, checkSufficientSignatures = false) + return stx.id + } +} diff --git a/node/src/integration-test/kotlin/net/corda/node/ContractWithGenericTypeTest.kt b/node/src/integration-test/kotlin/net/corda/node/ContractWithGenericTypeTest.kt new file mode 100644 index 0000000000..4a093de5ba --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/node/ContractWithGenericTypeTest.kt @@ -0,0 +1,52 @@ +package net.corda.node + +import net.corda.client.rpc.CordaRPCClient +import net.corda.contracts.serialization.generics.DataObject +import net.corda.core.messaging.startFlow +import net.corda.core.utilities.getOrThrow +import net.corda.core.utilities.loggerFor +import net.corda.flows.serialization.generics.GenericTypeFlow +import net.corda.node.services.Permissions +import net.corda.testing.core.ALICE_NAME +import net.corda.testing.core.DUMMY_NOTARY_NAME +import net.corda.testing.driver.DriverParameters +import net.corda.testing.driver.driver +import net.corda.testing.driver.internal.incrementalPortAllocation +import net.corda.testing.node.NotarySpec +import net.corda.testing.node.User +import net.corda.testing.node.internal.cordappWithPackages +import org.junit.Test + +@Suppress("FunctionName") +class ContractWithGenericTypeTest { + companion object { + const val DATA_VALUE = 5000L + + @JvmField + val logger = loggerFor() + } + + @Test(timeout=300_000) + fun `flow with generic type`() { + val user = User("u", "p", setOf(Permissions.all())) + driver(DriverParameters( + portAllocation = incrementalPortAllocation(), + startNodesInProcess = false, + notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, validating = true)), + cordappsForAllNodes = listOf( + cordappWithPackages("net.corda.flows.serialization.generics").signed(), + cordappWithPackages("net.corda.contracts.serialization.generics").signed() + ) + )) { + val alice = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow() + val txID = CordaRPCClient(hostAndPort = alice.rpcAddress) + .start(user.username, user.password) + .use { client -> + client.proxy.startFlow(::GenericTypeFlow, DataObject(DATA_VALUE)) + .returnValue + .getOrThrow() + } + logger.info("TX-ID=$txID") + } + } +} \ No newline at end of file diff --git a/node/src/integration-test/kotlin/net/corda/node/services/DeterministicContractWithGenericTypeTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/DeterministicContractWithGenericTypeTest.kt new file mode 100644 index 0000000000..b788091232 --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/node/services/DeterministicContractWithGenericTypeTest.kt @@ -0,0 +1,59 @@ +package net.corda.node.services + +import net.corda.client.rpc.CordaRPCClient +import net.corda.contracts.serialization.generics.DataObject +import net.corda.core.messaging.startFlow +import net.corda.core.utilities.getOrThrow +import net.corda.core.utilities.loggerFor +import net.corda.flows.serialization.generics.GenericTypeFlow +import net.corda.node.DeterministicSourcesRule +import net.corda.testing.core.ALICE_NAME +import net.corda.testing.core.DUMMY_NOTARY_NAME +import net.corda.testing.driver.DriverParameters +import net.corda.testing.driver.driver +import net.corda.testing.driver.internal.incrementalPortAllocation +import net.corda.testing.node.NotarySpec +import net.corda.testing.node.User +import net.corda.testing.node.internal.cordappWithPackages +import org.junit.ClassRule +import org.junit.Test + +@Suppress("FunctionName") +class DeterministicContractWithGenericTypeTest { + companion object { + const val DATA_VALUE = 5000L + + @JvmField + val logger = loggerFor() + + @ClassRule + @JvmField + val djvmSources = DeterministicSourcesRule() + } + + @Test(timeout=300_000) + fun `test DJVM can deserialise command with generic type`() { + val user = User("u", "p", setOf(Permissions.all())) + driver(DriverParameters( + portAllocation = incrementalPortAllocation(), + startNodesInProcess = false, + notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, validating = true)), + cordappsForAllNodes = listOf( + cordappWithPackages("net.corda.flows.serialization.generics").signed(), + cordappWithPackages("net.corda.contracts.serialization.generics").signed() + ), + djvmBootstrapSource = djvmSources.bootstrap, + djvmCordaSource = djvmSources.corda + )) { + val alice = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow() + val txID = CordaRPCClient(hostAndPort = alice.rpcAddress) + .start(user.username, user.password) + .use { client -> + client.proxy.startFlow(::GenericTypeFlow, DataObject(DATA_VALUE)) + .returnValue + .getOrThrow() + } + logger.info("TX-ID=$txID") + } + } +} \ No newline at end of file diff --git a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/SandboxSerializerFactoryFactory.kt b/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/SandboxSerializerFactoryFactory.kt index 95abdcd3fe..bafa2b8dea 100644 --- a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/SandboxSerializerFactoryFactory.kt +++ b/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/SandboxSerializerFactoryFactory.kt @@ -75,7 +75,7 @@ class SandboxSerializerFactoryFactory( ) ) - val fingerPrinter = TypeModellingFingerPrinter(customSerializerRegistry) + val fingerPrinter = TypeModellingFingerPrinter(customSerializerRegistry, classLoader) val localSerializerFactory = DefaultLocalSerializerFactory( whitelist = context.whitelist, diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/LocalSerializerFactory.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/LocalSerializerFactory.kt index 9cf064a70b..9b0ce7b9ae 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/LocalSerializerFactory.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/LocalSerializerFactory.kt @@ -7,7 +7,6 @@ import net.corda.core.utilities.debug import net.corda.core.utilities.trace import net.corda.serialization.internal.model.* import net.corda.serialization.internal.model.TypeIdentifier.* -import net.corda.serialization.internal.model.TypeIdentifier.Companion.classLoaderFor import org.apache.qpid.proton.amqp.Symbol import java.lang.reflect.ParameterizedType import java.lang.reflect.Type @@ -161,7 +160,7 @@ class DefaultLocalSerializerFactory( val declaredGenericType = if (declaredType !is ParameterizedType && localTypeInformation.typeIdentifier is Parameterised && declaredClass != Class::class.java) { - localTypeInformation.typeIdentifier.getLocalType(classLoaderFor(declaredClass)) + localTypeInformation.typeIdentifier.getLocalType(classloader) } else { declaredType } diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactoryBuilder.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactoryBuilder.kt index 4e7fbb466b..dac25a17ad 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactoryBuilder.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactoryBuilder.kt @@ -103,7 +103,7 @@ object SerializerFactoryBuilder { customSerializerRegistry)) val fingerPrinter = overrideFingerPrinter ?: - TypeModellingFingerPrinter(customSerializerRegistry) + TypeModellingFingerPrinter(customSerializerRegistry, classCarpenter.classloader) val localSerializerFactory = DefaultLocalSerializerFactory( whitelist, diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/model/TypeIdentifier.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/model/TypeIdentifier.kt index 2697b107a8..3477c02a48 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/model/TypeIdentifier.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/model/TypeIdentifier.kt @@ -45,12 +45,12 @@ sealed class TypeIdentifier { * Obtain a nicely-formatted representation of the identified type, for help with debugging. */ fun prettyPrint(simplifyClassNames: Boolean = true): String = when(this) { - is TypeIdentifier.UnknownType -> "?" - is TypeIdentifier.TopType -> "*" - is TypeIdentifier.Unparameterised -> name.simplifyClassNameIfRequired(simplifyClassNames) - is TypeIdentifier.Erased -> "${name.simplifyClassNameIfRequired(simplifyClassNames)} (erased)" - is TypeIdentifier.ArrayOf -> "${componentType.prettyPrint(simplifyClassNames)}[]" - is TypeIdentifier.Parameterised -> + is UnknownType -> "?" + is TopType -> "*" + is Unparameterised -> name.simplifyClassNameIfRequired(simplifyClassNames) + is Erased -> "${name.simplifyClassNameIfRequired(simplifyClassNames)} (erased)" + is ArrayOf -> "${componentType.prettyPrint(simplifyClassNames)}[]" + is Parameterised -> name.simplifyClassNameIfRequired(simplifyClassNames) + parameters.joinToString(", ", "<", ">") { it.prettyPrint(simplifyClassNames) } @@ -63,8 +63,6 @@ sealed class TypeIdentifier { // This method has locking. So we memo the value here. private val systemClassLoader: ClassLoader = ClassLoader.getSystemClassLoader() - fun classLoaderFor(clazz: Class<*>): ClassLoader = clazz.classLoader ?: systemClassLoader - /** * Obtain the [TypeIdentifier] for an erased Java class. * @@ -81,7 +79,7 @@ sealed class TypeIdentifier { * Obtain the [TypeIdentifier] for a Java [Type] (typically obtained by calling one of * [java.lang.reflect.Parameter.getAnnotatedType], * [java.lang.reflect.Field.getGenericType] or - * [java.lang.reflect.Method.getGenericReturnType]). Wildcard types and type variables are converted to [Unknown]. + * [java.lang.reflect.Method.getGenericReturnType]). Wildcard types and type variables are converted to [UnknownType]. * * @param type The [Type] to obtain a [TypeIdentifier] for. * @param resolutionContext Optionally, a [Type] which can be used to resolve type variables, for example a @@ -273,5 +271,5 @@ private class ReconstitutedParameterizedType( other.ownerType == ownerType && Arrays.equals(other.actualTypeArguments, actualTypeArguments) override fun hashCode(): Int = - Arrays.hashCode(actualTypeArguments) xor Objects.hashCode(ownerType) xor Objects.hashCode(rawType) + actualTypeArguments.contentHashCode() xor Objects.hashCode(ownerType) xor Objects.hashCode(rawType) } \ No newline at end of file diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/model/TypeModellingFingerPrinter.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/model/TypeModellingFingerPrinter.kt index c5d79ed41f..8965a5c8e1 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/model/TypeModellingFingerPrinter.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/model/TypeModellingFingerPrinter.kt @@ -5,7 +5,6 @@ import net.corda.core.utilities.contextLogger import net.corda.core.utilities.toBase64 import net.corda.serialization.internal.amqp.* import net.corda.serialization.internal.model.TypeIdentifier.* -import net.corda.serialization.internal.model.TypeIdentifier.Companion.classLoaderFor import java.lang.reflect.ParameterizedType /** @@ -31,6 +30,7 @@ interface FingerPrinter { */ class TypeModellingFingerPrinter( private val customTypeDescriptorLookup: CustomSerializerRegistry, + private val classLoader: ClassLoader, private val debugEnabled: Boolean = false) : FingerPrinter { private val cache: MutableMap = DefaultCacheProvider.createCache() @@ -42,7 +42,7 @@ class TypeModellingFingerPrinter( * the Fingerprinter cannot guarantee that. */ cache.getOrPut(typeInformation.typeIdentifier) { - FingerPrintingState(customTypeDescriptorLookup, FingerprintWriter(debugEnabled)) + FingerPrintingState(customTypeDescriptorLookup, classLoader, FingerprintWriter(debugEnabled)) .fingerprint(typeInformation) } } @@ -95,6 +95,7 @@ internal class FingerprintWriter(debugEnabled: Boolean = false) { */ private class FingerPrintingState( private val customSerializerRegistry: CustomSerializerRegistry, + private val classLoader: ClassLoader, private val writer: FingerprintWriter) { companion object { @@ -200,7 +201,7 @@ private class FingerPrintingState( private fun fingerprintName(type: LocalTypeInformation) { val identifier = type.typeIdentifier when (identifier) { - is TypeIdentifier.ArrayOf -> writer.write(identifier.componentType.name).writeArray() + is ArrayOf -> writer.write(identifier.componentType.name).writeArray() else -> writer.write(identifier.name) } } @@ -239,7 +240,7 @@ private class FingerPrintingState( val observedGenericType = if (observedType !is ParameterizedType && type.typeIdentifier is Parameterised && observedClass != Class::class.java) { - type.typeIdentifier.getLocalType(classLoaderFor(observedClass)) + type.typeIdentifier.getLocalType(classLoader) } else { observedType } @@ -259,6 +260,5 @@ private class FingerPrintingState( // and deserializing (assuming deserialization is occurring in a factory that didn't // serialise the object in the first place (and thus the cache lookup fails). This is also // true of Any, where we need Example and Example to have the same fingerprint - private fun hasSeen(type: TypeIdentifier) = (type in typesSeen) - && (type != TypeIdentifier.UnknownType) + private fun hasSeen(type: TypeIdentifier) = (type in typesSeen) && (type != UnknownType) } diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/TypeModellingFingerPrinterTests.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/TypeModellingFingerPrinterTests.kt index 362972afc7..84c3a27e63 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/TypeModellingFingerPrinterTests.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/TypeModellingFingerPrinterTests.kt @@ -12,7 +12,7 @@ class TypeModellingFingerPrinterTests { val descriptorBasedSerializerRegistry = DefaultDescriptorBasedSerializerRegistry() val customRegistry = CachingCustomSerializerRegistry(descriptorBasedSerializerRegistry) - val fingerprinter = TypeModellingFingerPrinter(customRegistry, true) + val fingerprinter = TypeModellingFingerPrinter(customRegistry, ClassLoader.getSystemClassLoader(), true) // See https://r3-cev.atlassian.net/browse/CORDA-2266 @Test(timeout=300_000) From 813e9dd242919d34a66aa5ce7c23352bf5954ae0 Mon Sep 17 00:00:00 2001 From: Razvan Codreanu <52859362+Schife@users.noreply.github.com> Date: Thu, 18 Jun 2020 12:52:33 +0100 Subject: [PATCH 11/85] INFRA-394 enable full junit log to avoid truncation (#6368) --- .ci/dev/integration/Jenkinsfile | 2 +- .ci/dev/nightly-regression/Jenkinsfile | 2 +- .ci/dev/regression/Jenkinsfile | 2 +- .ci/dev/smoke/Jenkinsfile | 2 +- .ci/dev/unit/Jenkinsfile | 2 +- Jenkinsfile | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.ci/dev/integration/Jenkinsfile b/.ci/dev/integration/Jenkinsfile index eba467e5a7..e8610ee716 100644 --- a/.ci/dev/integration/Jenkinsfile +++ b/.ci/dev/integration/Jenkinsfile @@ -53,7 +53,7 @@ pipeline { post { always { - junit '**/build/test-results-xml/**/*.xml' + junit testResults: '**/build/test-results-xml/**/*.xml', keepLongStdio: true } cleanup { deleteDir() /* clean up our workspace */ diff --git a/.ci/dev/nightly-regression/Jenkinsfile b/.ci/dev/nightly-regression/Jenkinsfile index a7f5d5ba17..62b2fcd820 100644 --- a/.ci/dev/nightly-regression/Jenkinsfile +++ b/.ci/dev/nightly-regression/Jenkinsfile @@ -79,7 +79,7 @@ pipeline { post { always { archiveArtifacts artifacts: '**/pod-logs/**/*.log', fingerprint: false - junit testResults: '**/build/test-results-xml/**/*.xml', allowEmptyResults: true + junit testResults: '**/build/test-results-xml/**/*.xml', allowEmptyResults: true, keepLongStdio: true } cleanup { deleteDir() /* clean up our workspace */ diff --git a/.ci/dev/regression/Jenkinsfile b/.ci/dev/regression/Jenkinsfile index 899f44684e..354f693f59 100644 --- a/.ci/dev/regression/Jenkinsfile +++ b/.ci/dev/regression/Jenkinsfile @@ -69,7 +69,7 @@ pipeline { post { always { archiveArtifacts artifacts: '**/pod-logs/**/*.log', fingerprint: false - junit '**/build/test-results-xml/**/*.xml' + junit testResults: '**/build/test-results-xml/**/*.xml', keepLongStdio: true script { try { diff --git a/.ci/dev/smoke/Jenkinsfile b/.ci/dev/smoke/Jenkinsfile index 3ddc3cdce8..aeb6638e95 100644 --- a/.ci/dev/smoke/Jenkinsfile +++ b/.ci/dev/smoke/Jenkinsfile @@ -52,7 +52,7 @@ pipeline { script { if (currentBuildTriggeredByComment()) { archiveArtifacts artifacts: '**/pod-logs/**/*.log', fingerprint: false - junit '**/build/test-results-xml/**/*.xml' + junit testResults: '**/build/test-results-xml/**/*.xml', keepLongStdio: true } } } diff --git a/.ci/dev/unit/Jenkinsfile b/.ci/dev/unit/Jenkinsfile index b2d2d54393..14b93f7425 100644 --- a/.ci/dev/unit/Jenkinsfile +++ b/.ci/dev/unit/Jenkinsfile @@ -51,7 +51,7 @@ pipeline { post { always { - junit '**/build/test-results-xml/**/*.xml' + junit testResults: '**/build/test-results-xml/**/*.xml', keepLongStdio: true } cleanup { deleteDir() /* clean up our workspace */ diff --git a/Jenkinsfile b/Jenkinsfile index 8048bed1ae..02365f00a4 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -68,7 +68,7 @@ pipeline { post { always { archiveArtifacts artifacts: '**/pod-logs/**/*.log', fingerprint: false - junit '**/build/test-results-xml/**/*.xml' + junit testResults: '**/build/test-results-xml/**/*.xml', keepLongStdio: true } cleanup { deleteDir() /* clean up our workspace */ From 56d0bbc03615d124d15aeab02a135c15c15af38e Mon Sep 17 00:00:00 2001 From: LankyDan Date: Thu, 18 Jun 2020 16:15:15 +0100 Subject: [PATCH 12/85] CORDA-3841 Check `isAnyCheckpointPersisted` in `startFlowInternal` (#6351) Only hit the database if `StateMachineState.isAnyCheckpointPersisted` returns true. Otherwise, there will be no checkpoint to retrieve from the database anyway. This can prevent errors due to a transient loss of connection to the database. Update tests after merging to 4.6 --- .../statemachine/StatemachineGeneralErrorHandlingTest.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineGeneralErrorHandlingTest.kt b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineGeneralErrorHandlingTest.kt index 79bc69aec0..e0732cd316 100644 --- a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineGeneralErrorHandlingTest.kt +++ b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineGeneralErrorHandlingTest.kt @@ -1125,7 +1125,7 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { assertEquals(3, discharge) assertEquals(0, observation) assertEquals(0, aliceClient.stateMachinesSnapshot().size) - assertEquals(1, aliceClient.startFlow(StatemachineErrorHandlingTest::GetNumberOfCheckpointsFlow).returnValue.get()) + assertEquals(1, aliceClient.startFlow(StatemachineErrorHandlingTest::GetNumberOfUncompletedCheckpointsFlow).returnValue.get()) } } @@ -1220,7 +1220,7 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { assertEquals(3, discharge) assertEquals(1, observation) assertEquals(1, aliceClient.stateMachinesSnapshot().size) - assertEquals(1, aliceClient.startFlow(StatemachineErrorHandlingTest::GetNumberOfCheckpointsFlow).returnValue.get()) + assertEquals(2, aliceClient.startFlow(StatemachineErrorHandlingTest::GetNumberOfUncompletedCheckpointsFlow).returnValue.get()) } } @@ -1612,7 +1612,7 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { assertEquals(0, observation) assertEquals(0, aliceClient.stateMachinesSnapshot().size) assertEquals(0, charlieClient.stateMachinesSnapshot().size) - assertEquals(1, charlieClient.startFlow(StatemachineErrorHandlingTest::GetNumberOfCheckpointsFlow).returnValue.get()) + assertEquals(1, charlieClient.startFlow(StatemachineErrorHandlingTest::GetNumberOfUncompletedCheckpointsFlow).returnValue.get()) } } @@ -1712,7 +1712,7 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { assertEquals(1, observation) assertEquals(1, aliceClient.stateMachinesSnapshot().size) assertEquals(1, charlieClient.stateMachinesSnapshot().size) - assertEquals(1, charlieClient.startFlow(StatemachineErrorHandlingTest::GetNumberOfCheckpointsFlow).returnValue.get()) + assertEquals(2, charlieClient.startFlow(StatemachineErrorHandlingTest::GetNumberOfUncompletedCheckpointsFlow).returnValue.get()) } } } \ No newline at end of file From 494073590b411014c096770a63a20c64b27a854b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Waldemar=20=C5=BBurowski?= <45210402+wzur-r3@users.noreply.github.com> Date: Thu, 18 Jun 2020 23:19:06 +0100 Subject: [PATCH 13/85] INFRA-347: Corda builds for Windows Servers (#6356) --- .ci/dev/mswin/Jenkinsfile | 93 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 .ci/dev/mswin/Jenkinsfile diff --git a/.ci/dev/mswin/Jenkinsfile b/.ci/dev/mswin/Jenkinsfile new file mode 100644 index 0000000000..8a7d19c5a3 --- /dev/null +++ b/.ci/dev/mswin/Jenkinsfile @@ -0,0 +1,93 @@ +#!groovy +/** + * Jenkins pipeline to build Corda on MS Windows server. + * Because it takes a long time to run tests sequentially, unit tests and + * integration tests are started in parallel on separate agents. + * + * Additionally, pull requests by default run only unit tests. + */ + +/** + * Kill already started job. + * Assume new commit takes precendence and results from previous + * unfinished builds are not required. + * This feature doesn't play well with disableConcurrentBuilds() option + */ +@Library('corda-shared-build-pipeline-steps') +import static com.r3.build.BuildControl.killAllExistingBuildsForJob +killAllExistingBuildsForJob(env.JOB_NAME, env.BUILD_NUMBER.toInteger()) + +/** + * Sense environment + */ +boolean isReleaseBranch = (env.BRANCH_NAME =~ /^release\/os\/.*/) + +pipeline { + agent none + options { + ansiColor('xterm') + timestamps() + timeout(time: 3, unit: 'HOURS') + } + + parameters { + booleanParam defaultValue: (isReleaseBranch), description: 'Run integration tests?', name: 'DO_INTEGRATION_TESTS' + } + + /* + * Do no receive Github's push events for release branches -> suitable for nightly builds + * but projects for pull requests will receive them as normal, and PR builds are started ASAP + */ + triggers { + pollSCM ignorePostCommitHooks: isReleaseBranch, scmpoll_spec: '@midnight' + } + + stages { + stage('Tests') { + parallel { + stage('Unit Tests') { + agent { label 'mswin' } + steps { + sh "./gradlew --no-daemon " + + "--stacktrace " + + "-Pcompilation.warningsAsErrors=false " + + "-Ptests.failFast=true " + + "clean test" + } + post { + always { + archiveArtifacts allowEmptyArchive: true, artifacts: '**/logs/**/*.log' + junit testResults: '**/build/test-results/**/*.xml', keepLongStdio: true, allowEmptyResults: true + bat '.ci/kill_corda_procs.cmd' + } + cleanup { + deleteDir() /* clean up our workspace */ + } + } + + } + stage('Integration Tests') { + when { + expression { params.DO_INTEGRATION_TESTS } + beforeAgent true + } + agent { label 'mswin' } + steps { + sh "./gradlew --no-daemon " + + "clean integrationTest" + } + post { + always { + archiveArtifacts allowEmptyArchive: true, artifacts: '**/logs/**/*.log' + junit testResults: '**/build/test-results/**/*.xml', keepLongStdio: true, allowEmptyResults: true + bat '.ci/kill_corda_procs.cmd' + } + cleanup { + deleteDir() /* clean up our workspace */ + } + } + } + } + } + } +} From 4bfb595e57f450d6cc158e98466b351830935d65 Mon Sep 17 00:00:00 2001 From: Euan Cairncross <35581327+cairncross@users.noreply.github.com> Date: Thu, 18 Jun 2020 23:46:29 +0100 Subject: [PATCH 14/85] INFRA-357 - Add release tag Jenkins build for OS 4.4 (#6362) --- .ci/dev/regression/Jenkinsfile | 82 ++++++++++++++++++++++------------ 1 file changed, 53 insertions(+), 29 deletions(-) diff --git a/.ci/dev/regression/Jenkinsfile b/.ci/dev/regression/Jenkinsfile index 354f693f59..be45291160 100644 --- a/.ci/dev/regression/Jenkinsfile +++ b/.ci/dev/regression/Jenkinsfile @@ -1,8 +1,24 @@ +#!groovy +/** + * Jenkins pipeline to build Corda OS release branches and tags + */ + +/** + * Kill already started job. + * Assume new commit takes precendence and results from previous + * unfinished builds are not required. + * This feature doesn't play well with disableConcurrentBuilds() option + */ @Library('corda-shared-build-pipeline-steps') import static com.r3.build.BuildControl.killAllExistingBuildsForJob killAllExistingBuildsForJob(env.JOB_NAME, env.BUILD_NUMBER.toInteger()) +/** + * Sense environment + */ +boolean isTag = (env.TAG_NAME =~ /^release-V(\d+\.\d+)?(\.\d+)?(-.+)?$/) + pipeline { agent { label 'k8s' } options { @@ -19,6 +35,12 @@ pipeline { } stages { + stage('Deploy Nodes') { + steps { + sh "./gradlew --no-daemon jar deployNodes" + } + } + stage('Generate Build Image') { steps { withCredentials([string(credentialsId: 'container_reg_passwd', variable: 'DOCKER_PUSH_PWD')]) { @@ -27,7 +49,7 @@ pipeline { "-Ddocker.push.password=\"\${DOCKER_PUSH_PWD}\" " + "-Ddocker.work.dir=\"/tmp/\${EXECUTOR_NUMBER}\" " + "-Ddocker.build.tag=\"\${DOCKER_TAG_TO_USE}\"" + - " clean preAllocateForParallelRegressionTest preAllocateForAllSlowIntegrationTest pushBuildImage --stacktrace" + " clean preAllocateForParallelRegressionTest preAllocateForAllParallelSlowIntegrationTest pushBuildImage --stacktrace" } sh "kubectl auth can-i get pods" } @@ -101,38 +123,40 @@ pipeline { } } } - + script { - // We want to send a summary email, but want to limit to once per day. - // Comparing the dates of the previous and current builds achieves this, - // i.e. we will only send an email for the first build on a given day. - def prevBuildDate = new Date( - currentBuild?.previousBuild.timeInMillis ?: 0).clearTime() - def currentBuildDate = new Date( - currentBuild.timeInMillis).clearTime() + if (!isTag) { + // We want to send a summary email, but want to limit to once per day. + // Comparing the dates of the previous and current builds achieves this, + // i.e. we will only send an email for the first build on a given day. + def prevBuildDate = new Date( + currentBuild?.previousBuild.timeInMillis ?: 0).clearTime() + def currentBuildDate = new Date( + currentBuild.timeInMillis).clearTime() - if (prevBuildDate != currentBuildDate) { - def statusSymbol = '\u2753' - switch(currentBuild.result) { - case 'SUCCESS': - statusSymbol = '\u2705' - break; - case 'UNSTABLE': - case 'FAILURE': - statusSymbol = '\u274c' - break; - default: - break; + if (prevBuildDate != currentBuildDate) { + def statusSymbol = '\u2753' + switch(currentBuild.result) { + case 'SUCCESS': + statusSymbol = '\u2705' + break; + case 'UNSTABLE': + case 'FAILURE': + statusSymbol = '\u274c' + break; + default: + break; + } + + echo('First build for this date, sending summary email') + emailext to: '$DEFAULT_RECIPIENTS', + subject: "$statusSymbol" + '$BRANCH_NAME regression tests - $BUILD_STATUS', + mimeType: 'text/html', + body: '${SCRIPT, template="groovy-html.template"}' + } else { + echo('Already sent summary email today, suppressing') } - - echo('First build for this date, sending summary email') - emailext to: '$DEFAULT_RECIPIENTS', - subject: "$statusSymbol" + '$BRANCH_NAME regression tests - $BUILD_STATUS', - mimeType: 'text/html', - body: '${SCRIPT, template="groovy-html.template"}' - } else { - echo('Already sent summary email today, suppressing') } } } From e77f7a7546b68188b8a9d4e732846ffc425a5462 Mon Sep 17 00:00:00 2001 From: alicer3 <50432430+alicer3@users.noreply.github.com> Date: Fri, 19 Jun 2020 16:49:07 +0800 Subject: [PATCH 15/85] center console message for registration (#6191) --- .../internal/subcommands/InitialRegistrationCli.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/node/src/main/kotlin/net/corda/node/internal/subcommands/InitialRegistrationCli.kt b/node/src/main/kotlin/net/corda/node/internal/subcommands/InitialRegistrationCli.kt index d0ea54969e..881de2c7df 100644 --- a/node/src/main/kotlin/net/corda/node/internal/subcommands/InitialRegistrationCli.kt +++ b/node/src/main/kotlin/net/corda/node/internal/subcommands/InitialRegistrationCli.kt @@ -62,11 +62,11 @@ class InitialRegistration(val baseDirectory: Path, private val networkRootTrustS val versionInfo = startup.getVersionInfo() println("\n" + - "******************************************************************\n" + - "* *\n" + - "* Registering as a new participant with a Corda network *\n" + - "* *\n" + - "******************************************************************\n") + "*******************************************************************\n" + + "* *\n" + + "* Registering as a new participant with a Corda network *\n" + + "* *\n" + + "*******************************************************************\n") NodeRegistrationHelper(NodeRegistrationConfiguration(conf), HTTPNetworkRegistrationService( From d6cab0e1312204674eccbbab1cac6c175d162c92 Mon Sep 17 00:00:00 2001 From: pnemeth Date: Fri, 19 Jun 2020 10:32:55 +0100 Subject: [PATCH 16/85] EG-1557 - Configuration data from "include" section ignored while command line contains the path to config file without leading ./ (#6354) Configuration data from "include" section ignored while command line contains the path to config file without leading ./ --- .../kotlin/net/corda/node/NodeCmdLineOptions.kt | 2 +- .../net/corda/node/internal/NodeStartupCliTest.kt | 14 +++++++++++++- .../kotlin/net/corda/cliutils/CordaCliWrapper.kt | 1 + 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/node/src/main/kotlin/net/corda/node/NodeCmdLineOptions.kt b/node/src/main/kotlin/net/corda/node/NodeCmdLineOptions.kt index d530ae4d41..0674cf757f 100644 --- a/node/src/main/kotlin/net/corda/node/NodeCmdLineOptions.kt +++ b/node/src/main/kotlin/net/corda/node/NodeCmdLineOptions.kt @@ -34,7 +34,7 @@ open class SharedNodeCmdLineOptions { description = ["The path to the config file. By default this is node.conf in the base directory."] ) private var _configFile: Path? = null - val configFile: Path get() = _configFile ?: (baseDirectory / "node.conf") + val configFile: Path get() = if (_configFile != null) baseDirectory.resolve(_configFile) else (baseDirectory / "node.conf") @Option( names = ["--on-unknown-config-keys"], diff --git a/node/src/test/kotlin/net/corda/node/internal/NodeStartupCliTest.kt b/node/src/test/kotlin/net/corda/node/internal/NodeStartupCliTest.kt index 119729ac76..1ebcbf93fb 100644 --- a/node/src/test/kotlin/net/corda/node/internal/NodeStartupCliTest.kt +++ b/node/src/test/kotlin/net/corda/node/internal/NodeStartupCliTest.kt @@ -22,7 +22,7 @@ class NodeStartupCliTest { companion object { private lateinit var workingDirectory: Path - + private var customNodeConf = "custom_node.conf" @BeforeClass @JvmStatic fun initDirectories() { @@ -56,6 +56,18 @@ class NodeStartupCliTest { Assertions.assertThat(startup.cmdLineOptions.networkRootTrustStorePathParameter).isEqualTo(null) } + @Test(timeout=300_000) + fun `--nodeconf using relative path will be changed to absolute path`() { + CommandLine.populateCommand(startup, CommonCliConstants.CONFIG_FILE, customNodeConf) + Assertions.assertThat(startup.cmdLineOptions.configFile).isEqualTo(workingDirectory / customNodeConf) + } + + @Test(timeout=300_000) + fun `--nodeconf using absolute path will not be changed`() { + CommandLine.populateCommand(startup, CommonCliConstants.CONFIG_FILE, "/$customNodeConf") + Assertions.assertThat(startup.cmdLineOptions.configFile).isEqualTo( "/" / customNodeConf) + } + @Test(timeout=3_000) @Ignore fun `test logs are written to correct location correctly if verbose flag set`() { diff --git a/tools/cliutils/src/main/kotlin/net/corda/cliutils/CordaCliWrapper.kt b/tools/cliutils/src/main/kotlin/net/corda/cliutils/CordaCliWrapper.kt index ce4d01ce3e..8bb9cb6432 100644 --- a/tools/cliutils/src/main/kotlin/net/corda/cliutils/CordaCliWrapper.kt +++ b/tools/cliutils/src/main/kotlin/net/corda/cliutils/CordaCliWrapper.kt @@ -211,6 +211,7 @@ fun printError(message: String) = System.err.println("${ShellConstants.RED}$mess */ object CommonCliConstants { const val BASE_DIR = "--base-directory" + const val CONFIG_FILE = "--config-file" } /** From ad21e381dc16af7c80c0ffbe3993edfb6e2666c1 Mon Sep 17 00:00:00 2001 From: Razvan Codreanu <52859362+Schife@users.noreply.github.com> Date: Fri, 19 Jun 2020 14:10:31 +0100 Subject: [PATCH 17/85] INFRA-357 Adding JDK 11 Regression and Build tag jenkinsfile (#6374) --- ...DockerfileJDK11Compile => DockerfileJDK11} | 0 .ci/dev/compatibility/JenkinsfileJDK11Azul | 121 ++++++++++++++++++ .ci/dev/compatibility/JenkinsfileJDK11Compile | 2 +- testing/DockerfileJDK11Azul | 3 + 4 files changed, 125 insertions(+), 1 deletion(-) rename .ci/dev/compatibility/{DockerfileJDK11Compile => DockerfileJDK11} (100%) create mode 100644 .ci/dev/compatibility/JenkinsfileJDK11Azul create mode 100644 testing/DockerfileJDK11Azul diff --git a/.ci/dev/compatibility/DockerfileJDK11Compile b/.ci/dev/compatibility/DockerfileJDK11 similarity index 100% rename from .ci/dev/compatibility/DockerfileJDK11Compile rename to .ci/dev/compatibility/DockerfileJDK11 diff --git a/.ci/dev/compatibility/JenkinsfileJDK11Azul b/.ci/dev/compatibility/JenkinsfileJDK11Azul new file mode 100644 index 0000000000..a61b5dc228 --- /dev/null +++ b/.ci/dev/compatibility/JenkinsfileJDK11Azul @@ -0,0 +1,121 @@ +@Library('corda-shared-build-pipeline-steps') +import static com.r3.build.BuildControl.killAllExistingBuildsForJob + +killAllExistingBuildsForJob(env.JOB_NAME, env.BUILD_NUMBER.toInteger()) + +/** + * Sense environment + */ +boolean isReleaseTag = (env.TAG_NAME =~ /^release.*JDK11$/) + +pipeline { + agent { + dockerfile { + label 'k8s' + additionalBuildArgs "--build-arg USER=stresstester" + filename '.ci/dev/compatibility/DockerfileJDK11' + } + } + options { + timestamps() + buildDiscarder(logRotator(daysToKeepStr: '7', artifactDaysToKeepStr: '7')) + timeout(time: 3, unit: 'HOURS') + } + + environment { + DOCKER_TAG_TO_USE = "${env.GIT_COMMIT.subSequence(0, 8)}JDK11" + EXECUTOR_NUMBER = "${env.EXECUTOR_NUMBER}" + BUILD_ID = "${env.BUILD_ID}-${env.JOB_NAME}" + ARTIFACTORY_CREDENTIALS = credentials('artifactory-credentials') + ARTIFACTORY_BUILD_NAME = "Corda / Publish / Publish JDK 11 Release to Artifactory".replaceAll("/", "::") + } + + stages { + stage('Generate Build Image') { + steps { + withCredentials([string(credentialsId: 'container_reg_passwd', variable: 'DOCKER_PUSH_PWD')]) { + sh "./gradlew " + + "-Dkubenetize=true " + + "-Ddocker.push.password=\"\${DOCKER_PUSH_PWD}\" " + + "-Ddocker.work.dir=\"/tmp/\${EXECUTOR_NUMBER}\" " + + "-Ddocker.build.tag=\"\${DOCKER_TAG_TO_USE}\" " + + "-Ddocker.buildbase.tag=11latest " + + "-Ddocker.dockerfile=DockerfileJDK11Azul" + + " clean pushBuildImage preAllocateForParallelRegressionTest preAllocateForAllParallelSlowIntegrationTest --stacktrace" + } + sh "kubectl auth can-i get pods" + } + } + + stage('Testing phase') { + parallel { + stage('Regression Test') { + steps { + sh "./gradlew " + + "-DbuildId=\"\${BUILD_ID}\" " + + "-Dkubenetize=true " + + "-Ddocker.run.tag=\"\${DOCKER_TAG_TO_USE}\" " + + "-Dartifactory.username=\"\${ARTIFACTORY_CREDENTIALS_USR}\" " + + "-Dartifactory.password=\"\${ARTIFACTORY_CREDENTIALS_PSW}\" " + + "-Dgit.branch=\"\${GIT_BRANCH}\" " + + "-Dgit.target.branch=\"\${GIT_BRANCH}\" " + + " parallelRegressionTest --stacktrace" + } + } + stage('Slow Integration Test') { + steps { + sh "./gradlew " + + "-DbuildId=\"\${BUILD_ID}\" " + + "-Dkubenetize=true " + + "-Ddocker.run.tag=\"\${DOCKER_TAG_TO_USE}\" " + + "-Dartifactory.username=\"\${ARTIFACTORY_CREDENTIALS_USR}\" " + + "-Dartifactory.password=\"\${ARTIFACTORY_CREDENTIALS_PSW}\" " + + "-Dgit.branch=\"\${GIT_BRANCH}\" " + + "-Dgit.target.branch=\"\${GIT_BRANCH}\" " + + " allParallelSlowIntegrationTest --stacktrace" + } + } + } + } + + stage('Publish to Artifactory') { + when { + expression { isReleaseTag } + } + steps { + rtServer( + id: 'R3-Artifactory', + url: 'https://software.r3.com/artifactory', + credentialsId: 'artifactory-credentials' + ) + rtGradleDeployer( + id: 'deployer', + serverId: 'R3-Artifactory', + repo: 'r3-corda-releases' + ) + rtGradleRun( + usesPlugin: true, + useWrapper: true, + switches: '-s --info', + tasks: 'artifactoryPublish', + deployerId: 'deployer', + buildName: env.ARTIFACTORY_BUILD_NAME + ) + rtPublishBuildInfo( + serverId: 'R3-Artifactory', + buildName: env.ARTIFACTORY_BUILD_NAME + ) + } + } + } + + post { + always { + archiveArtifacts artifacts: '**/pod-logs/**/*.log', fingerprint: false + junit '**/build/test-results-xml/**/*.xml' + } + cleanup { + deleteDir() /* clean up our workspace */ + } + } +} diff --git a/.ci/dev/compatibility/JenkinsfileJDK11Compile b/.ci/dev/compatibility/JenkinsfileJDK11Compile index 670717da68..d2251fad15 100644 --- a/.ci/dev/compatibility/JenkinsfileJDK11Compile +++ b/.ci/dev/compatibility/JenkinsfileJDK11Compile @@ -8,7 +8,7 @@ pipeline { dockerfile { label 'k8s' additionalBuildArgs "--build-arg USER=stresstester" - filename '.ci/dev/compatibility/DockerfileJDK11Compile' + filename '.ci/dev/compatibility/DockerfileJDK11' } } options { diff --git a/testing/DockerfileJDK11Azul b/testing/DockerfileJDK11Azul new file mode 100644 index 0000000000..83f3c324de --- /dev/null +++ b/testing/DockerfileJDK11Azul @@ -0,0 +1,3 @@ +FROM stefanotestingcr.azurecr.io/buildbase:11latest +COPY . /tmp/source +CMD cd /tmp/source && GRADLE_USER_HOME=/tmp/gradle ./gradlew clean testClasses integrationTestClasses --parallel --inf \ No newline at end of file From 618f7211bb6d6fd5020711034e44c57d8c3854f7 Mon Sep 17 00:00:00 2001 From: Razvan Codreanu <52859362+Schife@users.noreply.github.com> Date: Fri, 19 Jun 2020 18:25:08 +0100 Subject: [PATCH 18/85] NOTICK fix regression jenkinsfile typo (#6376) --- .ci/dev/regression/Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/dev/regression/Jenkinsfile b/.ci/dev/regression/Jenkinsfile index 995fd50f76..d21b89ea50 100644 --- a/.ci/dev/regression/Jenkinsfile +++ b/.ci/dev/regression/Jenkinsfile @@ -23,7 +23,7 @@ pipeline { "-Ddocker.push.password=\"\${DOCKER_PUSH_PWD}\" " + "-Ddocker.work.dir=\"/tmp/\${EXECUTOR_NUMBER}\" " + "-Ddocker.build.tag=\"\${DOCKER_TAG_TO_USE}\"" + - " clean preAllocateForParallelRegressionTest preAllocateForAllSlowIntegrationTest pushBuildImage --stacktrace" + " clean preAllocateForParallelRegressionTest preAllocateForAllParallelSlowIntegrationTest pushBuildImage --stacktrace" } sh "kubectl auth can-i get pods" } From 23f81e73a6299679fcd0a916a94a40d8490d7ed0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Waldemar=20=C5=BBurowski?= <45210402+wzur-r3@users.noreply.github.com> Date: Fri, 19 Jun 2020 19:30:11 +0100 Subject: [PATCH 19/85] Remove obsolete Jenkins configration for smoke tests (#6379) --- .ci/dev/smoke/Jenkinsfile | 101 -------------------------------------- 1 file changed, 101 deletions(-) delete mode 100644 .ci/dev/smoke/Jenkinsfile diff --git a/.ci/dev/smoke/Jenkinsfile b/.ci/dev/smoke/Jenkinsfile deleted file mode 100644 index 3ddc3cdce8..0000000000 --- a/.ci/dev/smoke/Jenkinsfile +++ /dev/null @@ -1,101 +0,0 @@ -@Library('corda-shared-build-pipeline-steps') -import static com.r3.build.BuildControl.killAllExistingBuildsForJob - -killAllExistingBuildsForJob(env.JOB_NAME, env.BUILD_NUMBER.toInteger()) - -pipeline { - agent { label 'k8s' } - options { - timestamps() - overrideIndexTriggers(false) - timeout(time: 3, unit: 'HOURS') - } - - triggers { - issueCommentTrigger('.*smoke tests.*') - } - - environment { - EXECUTOR_NUMBER = "${env.EXECUTOR_NUMBER}" - } - - stages { - stage('Corda Smoke Tests') { - steps { - script { - if (currentBuildTriggeredByComment()) { - stage('Run Smoke Tests') { - script { - pullRequest.createStatus(status: 'pending', - context: 'continuous-integration/jenkins/pr-merge/smokeTest', - description: 'Smoke Tests Running', - targetUrl: "${env.JOB_URL}") - } - - withCredentials([string(credentialsId: 'container_reg_passwd', variable: 'DOCKER_PUSH_PWD')]) { - sh "./gradlew " + - "-Dkubenetize=true " + - "-Ddocker.push.password=\"\${DOCKER_PUSH_PWD}\" " + - "-Ddocker.work.dir=\"/tmp/\${EXECUTOR_NUMBER}\" " + - " clean allParallelSmokeTest --stacktrace" - } - } - - } - } - } - } - } - - post { - always { - script { - if (currentBuildTriggeredByComment()) { - archiveArtifacts artifacts: '**/pod-logs/**/*.log', fingerprint: false - junit '**/build/test-results-xml/**/*.xml' - } - } - } - - - success { - script { - if (currentBuildTriggeredByComment()) { - pullRequest.createStatus(status: 'success', - context: 'continuous-integration/jenkins/pr-merge/smokeTest', - description: 'Smoke Tests Passed', - targetUrl: "${env.BUILD_URL}testResults") - } - } - } - - failure { - script { - if (currentBuildTriggeredByComment()) { - pullRequest.createStatus(status: 'failure', - context: 'continuous-integration/jenkins/pr-merge/smokeTest', - description: 'Smoke Tests Failed', - targetUrl: "${env.BUILD_URL}testResults") - } - } - } - - cleanup { - deleteDir() /* clean up our workspace */ - } - } -} - -@NonCPS -def currentBuildTriggeredByComment() { - def triggerCause = currentBuild.rawBuild.getCause(org.jenkinsci.plugins.pipeline.github.trigger.IssueCommentCause) - if (triggerCause) { - echo("Build was started by ${triggerCause.userLogin}, who wrote: " + - "\"${triggerCause.comment}\", which matches the " + - "\"${triggerCause.triggerPattern}\" trigger pattern.") - } else { - echo('Build was not started by a trigger') - } - - return triggerCause != null -} From ac0e35c8540258a51373f14221788290afe19e26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Waldemar=20=C5=BBurowski?= <45210402+wzur-r3@users.noreply.github.com> Date: Fri, 19 Jun 2020 22:03:09 +0100 Subject: [PATCH 20/85] Trying to ignore push events for Windows Server builds (#6383) --- .ci/dev/mswin/Jenkinsfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.ci/dev/mswin/Jenkinsfile b/.ci/dev/mswin/Jenkinsfile index 8a7d19c5a3..f09522578b 100644 --- a/.ci/dev/mswin/Jenkinsfile +++ b/.ci/dev/mswin/Jenkinsfile @@ -28,6 +28,9 @@ pipeline { ansiColor('xterm') timestamps() timeout(time: 3, unit: 'HOURS') +// overrideIndexTriggers true // works as expected for PRs +// overrideIndexTriggers false // works as expected for non PRs + overrideIndexTriggers (!isReleaseBranch) } parameters { From a7644dab8bada9dc5d7a4112a8934bd40380ec18 Mon Sep 17 00:00:00 2001 From: Waldemar Zurowski Date: Fri, 19 Jun 2020 22:09:02 +0100 Subject: [PATCH 21/85] Cosmetic change for Windows Server builds --- .ci/dev/mswin/Jenkinsfile | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.ci/dev/mswin/Jenkinsfile b/.ci/dev/mswin/Jenkinsfile index f09522578b..ffbfbcb5d6 100644 --- a/.ci/dev/mswin/Jenkinsfile +++ b/.ci/dev/mswin/Jenkinsfile @@ -28,8 +28,12 @@ pipeline { ansiColor('xterm') timestamps() timeout(time: 3, unit: 'HOURS') -// overrideIndexTriggers true // works as expected for PRs -// overrideIndexTriggers false // works as expected for non PRs + + /* + * a bit awkward to read + * is parameter is true -> push events are *not* ignored + * if parameter is false -> push events *are* ignored + */ overrideIndexTriggers (!isReleaseBranch) } From e021022d0d3e56cb76e1f85f2e4e5f8652011dbe Mon Sep 17 00:00:00 2001 From: Razvan Codreanu <52859362+Schife@users.noreply.github.com> Date: Sat, 20 Jun 2020 11:08:52 +0100 Subject: [PATCH 22/85] INFRA-357 Publish build tag (#6375) --- .ci/dev/compatibility/JenkinsfileJDK11Azul | 121 --------------------- .ci/dev/regression/Jenkinsfile | 35 +++++- testing/DockerfileJDK11Azul | 3 - 3 files changed, 33 insertions(+), 126 deletions(-) delete mode 100644 .ci/dev/compatibility/JenkinsfileJDK11Azul delete mode 100644 testing/DockerfileJDK11Azul diff --git a/.ci/dev/compatibility/JenkinsfileJDK11Azul b/.ci/dev/compatibility/JenkinsfileJDK11Azul deleted file mode 100644 index a61b5dc228..0000000000 --- a/.ci/dev/compatibility/JenkinsfileJDK11Azul +++ /dev/null @@ -1,121 +0,0 @@ -@Library('corda-shared-build-pipeline-steps') -import static com.r3.build.BuildControl.killAllExistingBuildsForJob - -killAllExistingBuildsForJob(env.JOB_NAME, env.BUILD_NUMBER.toInteger()) - -/** - * Sense environment - */ -boolean isReleaseTag = (env.TAG_NAME =~ /^release.*JDK11$/) - -pipeline { - agent { - dockerfile { - label 'k8s' - additionalBuildArgs "--build-arg USER=stresstester" - filename '.ci/dev/compatibility/DockerfileJDK11' - } - } - options { - timestamps() - buildDiscarder(logRotator(daysToKeepStr: '7', artifactDaysToKeepStr: '7')) - timeout(time: 3, unit: 'HOURS') - } - - environment { - DOCKER_TAG_TO_USE = "${env.GIT_COMMIT.subSequence(0, 8)}JDK11" - EXECUTOR_NUMBER = "${env.EXECUTOR_NUMBER}" - BUILD_ID = "${env.BUILD_ID}-${env.JOB_NAME}" - ARTIFACTORY_CREDENTIALS = credentials('artifactory-credentials') - ARTIFACTORY_BUILD_NAME = "Corda / Publish / Publish JDK 11 Release to Artifactory".replaceAll("/", "::") - } - - stages { - stage('Generate Build Image') { - steps { - withCredentials([string(credentialsId: 'container_reg_passwd', variable: 'DOCKER_PUSH_PWD')]) { - sh "./gradlew " + - "-Dkubenetize=true " + - "-Ddocker.push.password=\"\${DOCKER_PUSH_PWD}\" " + - "-Ddocker.work.dir=\"/tmp/\${EXECUTOR_NUMBER}\" " + - "-Ddocker.build.tag=\"\${DOCKER_TAG_TO_USE}\" " + - "-Ddocker.buildbase.tag=11latest " + - "-Ddocker.dockerfile=DockerfileJDK11Azul" + - " clean pushBuildImage preAllocateForParallelRegressionTest preAllocateForAllParallelSlowIntegrationTest --stacktrace" - } - sh "kubectl auth can-i get pods" - } - } - - stage('Testing phase') { - parallel { - stage('Regression Test') { - steps { - sh "./gradlew " + - "-DbuildId=\"\${BUILD_ID}\" " + - "-Dkubenetize=true " + - "-Ddocker.run.tag=\"\${DOCKER_TAG_TO_USE}\" " + - "-Dartifactory.username=\"\${ARTIFACTORY_CREDENTIALS_USR}\" " + - "-Dartifactory.password=\"\${ARTIFACTORY_CREDENTIALS_PSW}\" " + - "-Dgit.branch=\"\${GIT_BRANCH}\" " + - "-Dgit.target.branch=\"\${GIT_BRANCH}\" " + - " parallelRegressionTest --stacktrace" - } - } - stage('Slow Integration Test') { - steps { - sh "./gradlew " + - "-DbuildId=\"\${BUILD_ID}\" " + - "-Dkubenetize=true " + - "-Ddocker.run.tag=\"\${DOCKER_TAG_TO_USE}\" " + - "-Dartifactory.username=\"\${ARTIFACTORY_CREDENTIALS_USR}\" " + - "-Dartifactory.password=\"\${ARTIFACTORY_CREDENTIALS_PSW}\" " + - "-Dgit.branch=\"\${GIT_BRANCH}\" " + - "-Dgit.target.branch=\"\${GIT_BRANCH}\" " + - " allParallelSlowIntegrationTest --stacktrace" - } - } - } - } - - stage('Publish to Artifactory') { - when { - expression { isReleaseTag } - } - steps { - rtServer( - id: 'R3-Artifactory', - url: 'https://software.r3.com/artifactory', - credentialsId: 'artifactory-credentials' - ) - rtGradleDeployer( - id: 'deployer', - serverId: 'R3-Artifactory', - repo: 'r3-corda-releases' - ) - rtGradleRun( - usesPlugin: true, - useWrapper: true, - switches: '-s --info', - tasks: 'artifactoryPublish', - deployerId: 'deployer', - buildName: env.ARTIFACTORY_BUILD_NAME - ) - rtPublishBuildInfo( - serverId: 'R3-Artifactory', - buildName: env.ARTIFACTORY_BUILD_NAME - ) - } - } - } - - post { - always { - archiveArtifacts artifacts: '**/pod-logs/**/*.log', fingerprint: false - junit '**/build/test-results-xml/**/*.xml' - } - cleanup { - deleteDir() /* clean up our workspace */ - } - } -} diff --git a/.ci/dev/regression/Jenkinsfile b/.ci/dev/regression/Jenkinsfile index be45291160..62f9292f77 100644 --- a/.ci/dev/regression/Jenkinsfile +++ b/.ci/dev/regression/Jenkinsfile @@ -17,7 +17,7 @@ killAllExistingBuildsForJob(env.JOB_NAME, env.BUILD_NUMBER.toInteger()) /** * Sense environment */ -boolean isTag = (env.TAG_NAME =~ /^release-V(\d+\.\d+)?(\.\d+)?(-.+)?$/) +boolean isReleaseTag = (env.TAG_NAME =~ /^release-.*(? Date: Sat, 20 Jun 2020 11:25:20 +0100 Subject: [PATCH 23/85] INFRA-357 Re adding JDK 11 regression build file that was removed in the 4.4 to 4.5 merge --- .ci/dev/compatibility/JenkinsfileJDK11Azul | 124 +++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 .ci/dev/compatibility/JenkinsfileJDK11Azul diff --git a/.ci/dev/compatibility/JenkinsfileJDK11Azul b/.ci/dev/compatibility/JenkinsfileJDK11Azul new file mode 100644 index 0000000000..fd6767e1e0 --- /dev/null +++ b/.ci/dev/compatibility/JenkinsfileJDK11Azul @@ -0,0 +1,124 @@ +@Library('corda-shared-build-pipeline-steps') +import static com.r3.build.BuildControl.killAllExistingBuildsForJob + +killAllExistingBuildsForJob(env.JOB_NAME, env.BUILD_NUMBER.toInteger()) + +/** + * Sense environment + */ +boolean isReleaseTag = (env.TAG_NAME =~ /^release.*JDK11$/) + +pipeline { + agent { + label 'k8s' + } + options { + timestamps() + buildDiscarder(logRotator(daysToKeepStr: '7', artifactDaysToKeepStr: '7')) + timeout(time: 3, unit: 'HOURS') + } + + environment { + DOCKER_TAG_TO_USE = "${env.GIT_COMMIT.subSequence(0, 8)}JDK11" + EXECUTOR_NUMBER = "${env.EXECUTOR_NUMBER}" + BUILD_ID = "${env.BUILD_ID}-${env.JOB_NAME}" + ARTIFACTORY_CREDENTIALS = credentials('artifactory-credentials') + ARTIFACTORY_BUILD_NAME = "Corda / Publish / Publish JDK 11 Release to Artifactory".replaceAll("/", "::") + } + + stages { + stage('Generate Build Image') { + steps { + withCredentials([string(credentialsId: 'container_reg_passwd', variable: 'DOCKER_PUSH_PWD')]) { + sh "./gradlew " + + "-Dkubenetize=true " + + "-Ddocker.push.password=\"\${DOCKER_PUSH_PWD}\" " + + "-Ddocker.work.dir=\"/tmp/\${EXECUTOR_NUMBER}\" " + + "-Ddocker.build.tag=\"\${DOCKER_TAG_TO_USE}\" " + + "-Ddocker.buildbase.tag=11latest " + + "-Ddocker.dockerfile=DockerfileJDK11Azul" + + " clean pushBuildImage preAllocateForParallelRegressionTest preAllocateForAllParallelSlowIntegrationTest --stacktrace" + } + sh "kubectl auth can-i get pods" + } + } + + stage('Testing phase') { + parallel { + stage('Regression Test') { + steps { + sh "./gradlew " + + "-DbuildId=\"\${BUILD_ID}\" " + + "-Dkubenetize=true " + + "-Ddocker.run.tag=\"\${DOCKER_TAG_TO_USE}\" " + + "-Dartifactory.username=\"\${ARTIFACTORY_CREDENTIALS_USR}\" " + + "-Dartifactory.password=\"\${ARTIFACTORY_CREDENTIALS_PSW}\" " + + "-Dgit.branch=\"\${GIT_BRANCH}\" " + + "-Dgit.target.branch=\"\${GIT_BRANCH}\" " + + " parallelRegressionTest --stacktrace" + } + } + stage('Slow Integration Test') { + steps { + sh "./gradlew " + + "-DbuildId=\"\${BUILD_ID}\" " + + "-Dkubenetize=true " + + "-Ddocker.run.tag=\"\${DOCKER_TAG_TO_USE}\" " + + "-Dartifactory.username=\"\${ARTIFACTORY_CREDENTIALS_USR}\" " + + "-Dartifactory.password=\"\${ARTIFACTORY_CREDENTIALS_PSW}\" " + + "-Dgit.branch=\"\${GIT_BRANCH}\" " + + "-Dgit.target.branch=\"\${GIT_BRANCH}\" " + + " allParallelSlowIntegrationTest --stacktrace" + } + } + } + } + + stage('Publish to Artifactory') { + agent { + dockerfile { + reuseNode true + additionalBuildArgs "--build-arg USER=stresstester" + filename '.ci/dev/compatibility/DockerfileJDK11' + } + } + when { + expression { isReleaseTag } + } + steps { + rtServer( + id: 'R3-Artifactory', + url: 'https://software.r3.com/artifactory', + credentialsId: 'artifactory-credentials' + ) + rtGradleDeployer( + id: 'deployer', + serverId: 'R3-Artifactory', + repo: 'r3-corda-releases' + ) + rtGradleRun( + usesPlugin: true, + useWrapper: true, + switches: '-s --info', + tasks: 'artifactoryPublish', + deployerId: 'deployer', + buildName: env.ARTIFACTORY_BUILD_NAME + ) + rtPublishBuildInfo( + serverId: 'R3-Artifactory', + buildName: env.ARTIFACTORY_BUILD_NAME + ) + } + } + } + + post { + always { + archiveArtifacts artifacts: '**/pod-logs/**/*.log', fingerprint: false + junit '**/build/test-results-xml/**/*.xml' + } + cleanup { + deleteDir() /* clean up our workspace */ + } + } +} From 3377df2212b32509825814677447ae2e5fb26334 Mon Sep 17 00:00:00 2001 From: Waldemar Zurowski Date: Sat, 20 Jun 2020 17:26:57 +0100 Subject: [PATCH 24/85] Switched to CMD for Windows Server * when using shell there are huge performance penalty --- .ci/dev/mswin/Jenkinsfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.ci/dev/mswin/Jenkinsfile b/.ci/dev/mswin/Jenkinsfile index ffbfbcb5d6..923a83ff85 100644 --- a/.ci/dev/mswin/Jenkinsfile +++ b/.ci/dev/mswin/Jenkinsfile @@ -55,7 +55,7 @@ pipeline { stage('Unit Tests') { agent { label 'mswin' } steps { - sh "./gradlew --no-daemon " + + bat "./gradlew --no-daemon " + "--stacktrace " + "-Pcompilation.warningsAsErrors=false " + "-Ptests.failFast=true " + @@ -80,7 +80,7 @@ pipeline { } agent { label 'mswin' } steps { - sh "./gradlew --no-daemon " + + bat "./gradlew --no-daemon " + "clean integrationTest" } post { From 6485a025c78d6e27cb6d3390c9dd6c022418c100 Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Mon, 22 Jun 2020 16:51:40 +0100 Subject: [PATCH 25/85] ENT-5430: Increase test coverage when serializing Optional fields. (#6387) --- .../generics/GenericTypeContract.kt | 18 +++++-- .../serialization/generics/GenericTypeFlow.kt | 4 +- .../corda/node/ContractWithGenericTypeTest.kt | 54 ++++++++++++++----- ...eterministicContractWithGenericTypeTest.kt | 52 +++++++++++++----- 4 files changed, 95 insertions(+), 33 deletions(-) diff --git a/node/src/integration-test/kotlin/net/corda/contracts/serialization/generics/GenericTypeContract.kt b/node/src/integration-test/kotlin/net/corda/contracts/serialization/generics/GenericTypeContract.kt index 4fcdae9da3..38a236b28f 100644 --- a/node/src/integration-test/kotlin/net/corda/contracts/serialization/generics/GenericTypeContract.kt +++ b/node/src/integration-test/kotlin/net/corda/contracts/serialization/generics/GenericTypeContract.kt @@ -3,6 +3,7 @@ package net.corda.contracts.serialization.generics import net.corda.core.contracts.CommandData import net.corda.core.contracts.Contract import net.corda.core.contracts.ContractState +import net.corda.core.contracts.requireThat import net.corda.core.identity.AbstractParty import net.corda.core.transactions.LedgerTransaction import java.util.Optional @@ -10,14 +11,23 @@ import java.util.Optional @Suppress("unused") class GenericTypeContract : Contract { override fun verify(tx: LedgerTransaction) { - val state = tx.outputsOfType() - require(state.isNotEmpty()) { - "Requires at least one data state" + val states = tx.outputsOfType() + requireThat { + "Requires at least one data state" using states.isNotEmpty() + } + val purchases = tx.commandsOfType() + requireThat { + "Requires at least one purchase" using purchases.isNotEmpty() + } + for (purchase in purchases) { + requireThat { + "Purchase has a price" using purchase.value.price.isPresent + } } } @Suppress("CanBeParameter", "MemberVisibilityCanBePrivate") - class State(val owner: AbstractParty, val data: DataObject) : ContractState { + class State(val owner: AbstractParty, val data: DataObject?) : ContractState { override val participants: List = listOf(owner) @Override diff --git a/node/src/integration-test/kotlin/net/corda/flows/serialization/generics/GenericTypeFlow.kt b/node/src/integration-test/kotlin/net/corda/flows/serialization/generics/GenericTypeFlow.kt index 2325d767b0..b5dc4abe29 100644 --- a/node/src/integration-test/kotlin/net/corda/flows/serialization/generics/GenericTypeFlow.kt +++ b/node/src/integration-test/kotlin/net/corda/flows/serialization/generics/GenericTypeFlow.kt @@ -12,14 +12,14 @@ import net.corda.core.transactions.TransactionBuilder import java.util.Optional @StartableByRPC -class GenericTypeFlow(private val purchase: DataObject) : FlowLogic() { +class GenericTypeFlow(private val purchase: DataObject?) : FlowLogic() { @Suspendable override fun call(): SecureHash { val notary = serviceHub.networkMapCache.notaryIdentities[0] val stx = serviceHub.signInitialTransaction( TransactionBuilder(notary) .addOutputState(State(ourIdentity, purchase)) - .addCommand(Command(Purchase(Optional.of(purchase)), ourIdentity.owningKey)) + .addCommand(Command(Purchase(Optional.ofNullable(purchase)), ourIdentity.owningKey)) ) stx.verify(serviceHub, checkSufficientSignatures = false) return stx.id diff --git a/node/src/integration-test/kotlin/net/corda/node/ContractWithGenericTypeTest.kt b/node/src/integration-test/kotlin/net/corda/node/ContractWithGenericTypeTest.kt index 4a093de5ba..d23c137dda 100644 --- a/node/src/integration-test/kotlin/net/corda/node/ContractWithGenericTypeTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/ContractWithGenericTypeTest.kt @@ -2,6 +2,7 @@ package net.corda.node import net.corda.client.rpc.CordaRPCClient import net.corda.contracts.serialization.generics.DataObject +import net.corda.core.contracts.TransactionVerificationException.ContractRejection import net.corda.core.messaging.startFlow import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.loggerFor @@ -15,7 +16,9 @@ import net.corda.testing.driver.internal.incrementalPortAllocation import net.corda.testing.node.NotarySpec import net.corda.testing.node.User import net.corda.testing.node.internal.cordappWithPackages +import org.assertj.core.api.Assertions.assertThat import org.junit.Test +import org.junit.jupiter.api.assertThrows @Suppress("FunctionName") class ContractWithGenericTypeTest { @@ -24,29 +27,52 @@ class ContractWithGenericTypeTest { @JvmField val logger = loggerFor() + + @JvmField + val user = User("u", "p", setOf(Permissions.all())) + + fun parameters(): DriverParameters { + return DriverParameters( + portAllocation = incrementalPortAllocation(), + startNodesInProcess = false, + notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, validating = true)), + cordappsForAllNodes = listOf( + cordappWithPackages("net.corda.flows.serialization.generics").signed(), + cordappWithPackages("net.corda.contracts.serialization.generics").signed() + ) + ) + } } - @Test(timeout=300_000) - fun `flow with generic type`() { - val user = User("u", "p", setOf(Permissions.all())) - driver(DriverParameters( - portAllocation = incrementalPortAllocation(), - startNodesInProcess = false, - notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, validating = true)), - cordappsForAllNodes = listOf( - cordappWithPackages("net.corda.flows.serialization.generics").signed(), - cordappWithPackages("net.corda.contracts.serialization.generics").signed() - ) - )) { + @Test(timeout = 300_000) + fun `flow with value of generic type`() { + driver(parameters()) { val alice = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow() val txID = CordaRPCClient(hostAndPort = alice.rpcAddress) .start(user.username, user.password) .use { client -> client.proxy.startFlow(::GenericTypeFlow, DataObject(DATA_VALUE)) - .returnValue - .getOrThrow() + .returnValue + .getOrThrow() } logger.info("TX-ID=$txID") } } + + @Test(timeout = 300_000) + fun `flow without value of generic type`() { + driver(parameters()) { + val alice = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow() + val ex = assertThrows { + CordaRPCClient(hostAndPort = alice.rpcAddress) + .start(user.username, user.password) + .use { client -> + client.proxy.startFlow(::GenericTypeFlow, null) + .returnValue + .getOrThrow() + } + } + assertThat(ex).hasMessageContaining("Contract verification failed: Failed requirement: Purchase has a price,") + } + } } \ No newline at end of file diff --git a/node/src/integration-test/kotlin/net/corda/node/services/DeterministicContractWithGenericTypeTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/DeterministicContractWithGenericTypeTest.kt index b788091232..c3c440eaf6 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/DeterministicContractWithGenericTypeTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/DeterministicContractWithGenericTypeTest.kt @@ -7,6 +7,7 @@ import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.loggerFor import net.corda.flows.serialization.generics.GenericTypeFlow import net.corda.node.DeterministicSourcesRule +import net.corda.node.internal.djvm.DeterministicVerificationException import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.DUMMY_NOTARY_NAME import net.corda.testing.driver.DriverParameters @@ -15,8 +16,10 @@ import net.corda.testing.driver.internal.incrementalPortAllocation import net.corda.testing.node.NotarySpec import net.corda.testing.node.User import net.corda.testing.node.internal.cordappWithPackages +import org.assertj.core.api.Assertions.assertThat import org.junit.ClassRule import org.junit.Test +import org.junit.jupiter.api.assertThrows @Suppress("FunctionName") class DeterministicContractWithGenericTypeTest { @@ -26,25 +29,31 @@ class DeterministicContractWithGenericTypeTest { @JvmField val logger = loggerFor() + @JvmField + val user = User("u", "p", setOf(Permissions.all())) + @ClassRule @JvmField val djvmSources = DeterministicSourcesRule() + + fun parameters(): DriverParameters { + return DriverParameters( + portAllocation = incrementalPortAllocation(), + startNodesInProcess = false, + notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, validating = true)), + cordappsForAllNodes = listOf( + cordappWithPackages("net.corda.flows.serialization.generics").signed(), + cordappWithPackages("net.corda.contracts.serialization.generics").signed() + ), + djvmBootstrapSource = djvmSources.bootstrap, + djvmCordaSource = djvmSources.corda + ) + } } - @Test(timeout=300_000) + @Test(timeout = 300_000) fun `test DJVM can deserialise command with generic type`() { - val user = User("u", "p", setOf(Permissions.all())) - driver(DriverParameters( - portAllocation = incrementalPortAllocation(), - startNodesInProcess = false, - notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, validating = true)), - cordappsForAllNodes = listOf( - cordappWithPackages("net.corda.flows.serialization.generics").signed(), - cordappWithPackages("net.corda.contracts.serialization.generics").signed() - ), - djvmBootstrapSource = djvmSources.bootstrap, - djvmCordaSource = djvmSources.corda - )) { + driver(parameters()) { val alice = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow() val txID = CordaRPCClient(hostAndPort = alice.rpcAddress) .start(user.username, user.password) @@ -56,4 +65,21 @@ class DeterministicContractWithGenericTypeTest { logger.info("TX-ID=$txID") } } + + @Test(timeout = 300_000) + fun `test DJVM can deserialise command without value of generic type`() { + driver(parameters()) { + val alice = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow() + val ex = assertThrows { + CordaRPCClient(hostAndPort = alice.rpcAddress) + .start(user.username, user.password) + .use { client -> + client.proxy.startFlow(::GenericTypeFlow, null) + .returnValue + .getOrThrow() + } + } + assertThat(ex).hasMessageContaining("Contract verification failed: Failed requirement: Purchase has a price,") + } + } } \ No newline at end of file From 6ec2910f15a07b487265e83db935928f9809a406 Mon Sep 17 00:00:00 2001 From: Kyriakos Tharrouniatis Date: Thu, 25 Jun 2020 09:40:58 +0100 Subject: [PATCH 26/85] NOTICK Revert node_flow_exceptions type length back to 256 (#6395) --- .../resources/migration/node-core.changelog-v19-postgres.xml | 2 +- node/src/main/resources/migration/node-core.changelog-v19.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/node/src/main/resources/migration/node-core.changelog-v19-postgres.xml b/node/src/main/resources/migration/node-core.changelog-v19-postgres.xml index 3f9ed5cab1..3c8e4d92ad 100644 --- a/node/src/main/resources/migration/node-core.changelog-v19-postgres.xml +++ b/node/src/main/resources/migration/node-core.changelog-v19-postgres.xml @@ -69,7 +69,7 @@ - + diff --git a/node/src/main/resources/migration/node-core.changelog-v19.xml b/node/src/main/resources/migration/node-core.changelog-v19.xml index cba014503c..761bdef715 100644 --- a/node/src/main/resources/migration/node-core.changelog-v19.xml +++ b/node/src/main/resources/migration/node-core.changelog-v19.xml @@ -69,7 +69,7 @@ - + From d720c86fc762e17474c72763e535cf40cda0daa6 Mon Sep 17 00:00:00 2001 From: Ryan Fowler Date: Thu, 25 Jun 2020 11:23:18 +0100 Subject: [PATCH 27/85] CORDA-3714: Update commons-beanutils for security (#6391) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 8ae347da27..6a3c888a5b 100644 --- a/build.gradle +++ b/build.gradle @@ -105,7 +105,7 @@ buildscript { ext.eddsa_version = '0.3.0' ext.dependency_checker_version = '5.2.0' ext.commons_collections_version = '4.3' - ext.beanutils_version = '1.9.3' + ext.beanutils_version = '1.9.4' ext.crash_version = '1.7.4' ext.jsr305_version = constants.getProperty("jsr305Version") ext.shiro_version = '1.4.1' From ef582900cf16ceb7e358b05802e8b69b0a0e8d99 Mon Sep 17 00:00:00 2001 From: Ryan Fowler Date: Thu, 25 Jun 2020 17:26:55 +0100 Subject: [PATCH 28/85] NOTICK Expand the regex to match what we already do in ENT (#6400) --- node/src/integration-test/kotlin/net/corda/node/NodeRPCTests.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/src/integration-test/kotlin/net/corda/node/NodeRPCTests.kt b/node/src/integration-test/kotlin/net/corda/node/NodeRPCTests.kt index f59887f803..bceb35ecef 100644 --- a/node/src/integration-test/kotlin/net/corda/node/NodeRPCTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/NodeRPCTests.kt @@ -11,7 +11,7 @@ import kotlin.test.assertEquals import kotlin.test.assertTrue class NodeRPCTests { - private val CORDA_VERSION_REGEX = "\\d+(\\.\\d+)?(-\\w+)?".toRegex() + private val CORDA_VERSION_REGEX = "\\d+(\\.\\d+)?(\\.\\d+)?(-\\w+)?".toRegex() private val CORDA_VENDOR = "Corda Open Source" private val CORDAPPS = listOf(FINANCE_CONTRACTS_CORDAPP, FINANCE_WORKFLOWS_CORDAPP) private val CORDAPP_TYPES = setOf("Contract CorDapp", "Workflow CorDapp") From b21a3c33cdcc7481057a602539323afb7fe2d46f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Waldemar=20=C5=BBurowski?= <45210402+wzur-r3@users.noreply.github.com> Date: Fri, 26 Jun 2020 10:48:47 +0100 Subject: [PATCH 29/85] Added Sonatype check for regression builds (#6401) --- .ci/dev/regression/Jenkinsfile | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/.ci/dev/regression/Jenkinsfile b/.ci/dev/regression/Jenkinsfile index 62f9292f77..271613a6ce 100644 --- a/.ci/dev/regression/Jenkinsfile +++ b/.ci/dev/regression/Jenkinsfile @@ -18,6 +18,20 @@ killAllExistingBuildsForJob(env.JOB_NAME, env.BUILD_NUMBER.toInteger()) * Sense environment */ boolean isReleaseTag = (env.TAG_NAME =~ /^release-.*(?version-properties" + def version = sh (returnStdout: true, script: "grep ^version: version-properties | sed -e 's/^version: //'").trim() + def groupId = sh (returnStdout: true, script: "grep ^group: version-properties | sed -e 's/^group: //'").trim() + def artifactId = 'corda' + nexusAppId = "jenkins-${groupId}-${artifactId}-${version}" + } + nexusPolicyEvaluation ( + failBuildOnNetworkError: false, + iqApplication: manualApplication(nexusAppId), + iqScanPatterns: [[scanPattern: 'node/capsule/build/libs/corda*.jar']], + iqStage: nexusIqStage + ) + } + } + stage('Deploy Nodes') { steps { sh "./gradlew --no-daemon jar deployNodes" From 796e92b51246c14d6976513b379f89abe2d697ac Mon Sep 17 00:00:00 2001 From: Dan Newton Date: Fri, 26 Jun 2020 12:48:15 +0100 Subject: [PATCH 30/85] CORDA-3720 Extract locking of InnerState out of SMM (#6289) The state machines state is held within `InnerState` which lived inside the SMM. `InnerState` has been extracted out of the SMM to allow the SMM to be refactored in the future. Smaller classes can now be made that focus on a single goal as the locking of the state can be accessed from external classes. To achieve this, pass the `InnerState` into the class and request a lock if needed. The locking of `InnerState` has been made a property of the `InnerState` itself. It has a `lock` field that allows locks to be taken out when needed. An inline `withLock` function has been added to tidy up the code and not harm performance. Some classes have been made internal to prevent invalid usage of purely node internal classes. As part of this change, flow timeouts have been extracted out into `FlowTimeoutScheduler`. --- .../StatemachineErrorHandlingTest.kt | 8 + .../StatemachineFinalityErrorHandlingTest.kt | 8 +- .../StatemachineGeneralErrorHandlingTest.kt | 76 +++---- .../StatemachineKillFlowErrorHandlingTest.kt | 4 +- .../StatemachineSubflowErrorHandlingTest.kt | 20 +- .../statemachine/ActionExecutorImpl.kt | 2 +- .../statemachine/FlowSleepScheduler.kt | 28 +-- .../statemachine/FlowTimeoutScheduler.kt | 101 +++++++++ .../SingleThreadedStateMachineManager.kt | 203 ++++-------------- .../statemachine/StateMachineInnerState.kt | 44 ++++ .../statemachine/StateMachineManager.kt | 3 +- .../utilities/StateMachineManagerUtils.kt | 2 +- 12 files changed, 267 insertions(+), 232 deletions(-) create mode 100644 node/src/main/kotlin/net/corda/node/services/statemachine/FlowTimeoutScheduler.kt create mode 100644 node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineInnerState.kt diff --git a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineErrorHandlingTest.kt b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineErrorHandlingTest.kt index 7ed4ce5325..d27a71430a 100644 --- a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineErrorHandlingTest.kt +++ b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineErrorHandlingTest.kt @@ -163,4 +163,12 @@ abstract class StatemachineErrorHandlingTest { } } } + + internal val actionExecutorClassName: String by lazy { + Class.forName("net.corda.node.services.statemachine.ActionExecutorImpl").name + } + + internal val stateMachineManagerClassName: String by lazy { + Class.forName("net.corda.node.services.statemachine.SingleThreadedStateMachineManager").name + } } \ No newline at end of file diff --git a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineFinalityErrorHandlingTest.kt b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineFinalityErrorHandlingTest.kt index 98e199afe2..634e45c64c 100644 --- a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineFinalityErrorHandlingTest.kt +++ b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineFinalityErrorHandlingTest.kt @@ -185,7 +185,7 @@ class StatemachineFinalityErrorHandlingTest : StatemachineErrorHandlingTest() { val rules = """ RULE Create Counter - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF createCounter("counter", $counter) @@ -201,7 +201,7 @@ class StatemachineFinalityErrorHandlingTest : StatemachineErrorHandlingTest() { ENDRULE RULE Throw exception on executeCommitTransaction action - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF flagged("finality_flag") && readCounter("counter") < 5 @@ -280,7 +280,7 @@ class StatemachineFinalityErrorHandlingTest : StatemachineErrorHandlingTest() { val rules = """ RULE Create Counter - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF createCounter("counter", $counter) @@ -296,7 +296,7 @@ class StatemachineFinalityErrorHandlingTest : StatemachineErrorHandlingTest() { ENDRULE RULE Throw exception on executeCommitTransaction action - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF flagged("finality_flag") && readCounter("counter") < 7 diff --git a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineGeneralErrorHandlingTest.kt b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineGeneralErrorHandlingTest.kt index e0732cd316..6040df4ed9 100644 --- a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineGeneralErrorHandlingTest.kt +++ b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineGeneralErrorHandlingTest.kt @@ -40,7 +40,7 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { val rules = """ RULE Create Counter - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeSendMultiple AT ENTRY IF createCounter("counter", $counter) @@ -48,7 +48,7 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { ENDRULE RULE Throw exception on executeSendMultiple action - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeSendMultiple AT ENTRY IF readCounter("counter") < 4 @@ -123,7 +123,7 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { val rules = """ RULE Create Counter - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeSendMultiple AT ENTRY IF createCounter("counter", $counter) @@ -131,7 +131,7 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { ENDRULE RULE Throw exception on executeSendMultiple action - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeSendMultiple AT ENTRY IF readCounter("counter") < 3 @@ -206,7 +206,7 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { val rules = """ RULE Set flag when inside executeAcknowledgeMessages - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeAcknowledgeMessages AT INVOKE ${DeduplicationHandler::class.java.name}.afterDatabaseTransaction() IF !flagged("exception_flag") @@ -294,7 +294,7 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { val rules = """ RULE Create Counter - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF createCounter("counter", $counter) @@ -302,7 +302,7 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { ENDRULE RULE Throw exception on executeCommitTransaction action - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF readCounter("counter") < 5 @@ -384,7 +384,7 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { val rules = """ RULE Create Counter - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF createCounter("counter", $counter) @@ -392,7 +392,7 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { ENDRULE RULE Throw exception on executeCommitTransaction action - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF readCounter("counter") < 7 @@ -474,7 +474,7 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { // seems to be restarting the flow from the beginning every time val rules = """ RULE Create Counter - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF createCounter("counter", $counter) @@ -490,7 +490,7 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { ENDRULE RULE Throw exception on executeCommitTransaction action after first suspend + commit - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF flagged("suspend_flag") && flagged("commit_flag") && readCounter("counter") < 5 @@ -498,7 +498,7 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { ENDRULE RULE Set flag when executing first commit - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF flagged("suspend_flag") && !flagged("commit_flag") @@ -574,7 +574,7 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { // seems to be restarting the flow from the beginning every time val rules = """ RULE Create Counter - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF createCounter("counter", $counter) @@ -590,7 +590,7 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { ENDRULE RULE Throw exception on executeCommitTransaction when removing checkpoint - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF flagged("remove_checkpoint_flag") && readCounter("counter") < 3 @@ -673,7 +673,7 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { ENDRULE RULE Throw exception on executeCommitTransaction action after first suspend + commit - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF flagged("suspend_flag") && flagged("commit_flag") && !flagged("commit_exception_flag") @@ -681,7 +681,7 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { ENDRULE RULE Set flag when executing first commit - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF flagged("suspend_flag") && !flagged("commit_flag") @@ -689,7 +689,7 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { ENDRULE RULE Throw exception on retry - CLASS ${SingleThreadedStateMachineManager::class.java.name} + CLASS $stateMachineManagerClassName METHOD addAndStartFlow AT ENTRY IF flagged("suspend_flag") && flagged("commit_flag") && !flagged("retry_exception_flag") @@ -776,7 +776,7 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { ENDRULE RULE Throw exception on executeCommitTransaction action after first suspend + commit - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF flagged("suspend_flag") && flagged("commit_flag") && !flagged("commit_exception_flag") @@ -784,7 +784,7 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { ENDRULE RULE Set flag when executing first commit - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF flagged("suspend_flag") && !flagged("commit_flag") @@ -792,7 +792,7 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { ENDRULE RULE Throw exception on retry - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF flagged("suspend_flag") && flagged("commit_exception_flag") && !flagged("retry_exception_flag") @@ -871,7 +871,7 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { val rules = """ RULE Throw exception on executeCommitTransaction action after first suspend + commit - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF !flagged("commit_exception_flag") @@ -879,7 +879,7 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { ENDRULE RULE Throw exception on retry - CLASS ${SingleThreadedStateMachineManager::class.java.name} + CLASS $stateMachineManagerClassName METHOD onExternalStartFlow AT ENTRY IF flagged("commit_exception_flag") && !flagged("retry_exception_flag") @@ -955,7 +955,7 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { val rules = """ RULE Create Counter - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF createCounter("counter", $counter) @@ -971,7 +971,7 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { ENDRULE RULE Throw exception on executeCommitTransaction when removing checkpoint - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF flagged("remove_checkpoint_flag") && readCounter("counter") < 4 @@ -1056,7 +1056,7 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { val rules = """ RULE Create Counter - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF createCounter("counter", $counter) @@ -1064,7 +1064,7 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { ENDRULE RULE Throw exception on executeCommitTransaction action - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF readCounter("counter") < 5 @@ -1151,7 +1151,7 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { val rules = """ RULE Create Counter - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF createCounter("counter", $counter) @@ -1159,7 +1159,7 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { ENDRULE RULE Throw exception on executeCommitTransaction action - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF readCounter("counter") < 7 @@ -1247,7 +1247,7 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { val rules = """ RULE Create Counter - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF createCounter("counter", $counter) @@ -1255,7 +1255,7 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { ENDRULE RULE Throw exception on executeCommitTransaction action - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF readCounter("counter") < 5 @@ -1345,7 +1345,7 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { val rules = """ RULE Create Counter - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF createCounter("counter", $counter) @@ -1353,7 +1353,7 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { ENDRULE RULE Throw exception on executeCommitTransaction action - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF readCounter("counter") < 7 @@ -1436,7 +1436,7 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { val rules = """ RULE Create Counter - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF createCounter("counter", $counter) @@ -1452,7 +1452,7 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { ENDRULE RULE Throw exception on executeCommitTransaction when removing checkpoint - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF flagged("remove_checkpoint_flag") && readCounter("counter") < 3 @@ -1540,7 +1540,7 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { val rules = """ RULE Create Counter - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF createCounter("counter", $counter) @@ -1548,7 +1548,7 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { ENDRULE RULE Throw exception on executeCommitTransaction action - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF readCounter("counter") < 5 @@ -1638,7 +1638,7 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { val rules = """ RULE Create Counter - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF createCounter("counter", $counter) @@ -1646,7 +1646,7 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { ENDRULE RULE Throw exception on executeCommitTransaction action - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF readCounter("counter") < 7 diff --git a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineKillFlowErrorHandlingTest.kt b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineKillFlowErrorHandlingTest.kt index 0d0c8f7177..2f3eb65e5d 100644 --- a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineKillFlowErrorHandlingTest.kt +++ b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineKillFlowErrorHandlingTest.kt @@ -206,7 +206,7 @@ class StatemachineKillFlowErrorHandlingTest : StatemachineErrorHandlingTest() { val rules = """ RULE Create Counter - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeSendMultiple AT ENTRY IF createCounter("counter", $counter) @@ -214,7 +214,7 @@ class StatemachineKillFlowErrorHandlingTest : StatemachineErrorHandlingTest() { ENDRULE RULE Throw exception on executeSendMultiple action - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeSendMultiple AT ENTRY IF readCounter("counter") < 4 diff --git a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineSubflowErrorHandlingTest.kt b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineSubflowErrorHandlingTest.kt index 161f3c4b39..ea9aad7351 100644 --- a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineSubflowErrorHandlingTest.kt +++ b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineSubflowErrorHandlingTest.kt @@ -45,7 +45,7 @@ class StatemachineSubflowErrorHandlingTest : StatemachineErrorHandlingTest() { val rules = """ RULE Create Counter - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF createCounter("counter", $counter) @@ -69,7 +69,7 @@ class StatemachineSubflowErrorHandlingTest : StatemachineErrorHandlingTest() { ENDRULE RULE Throw exception on executeCommitTransaction action after first suspend + commit - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF flagged("subflow_flag") && flagged("suspend_flag") && flagged("commit_flag") && readCounter("counter") < 5 @@ -77,7 +77,7 @@ class StatemachineSubflowErrorHandlingTest : StatemachineErrorHandlingTest() { ENDRULE RULE Set flag when executing first commit - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF flagged("subflow_flag") && flagged("suspend_flag") && !flagged("commit_flag") @@ -155,7 +155,7 @@ class StatemachineSubflowErrorHandlingTest : StatemachineErrorHandlingTest() { val rules = """ RULE Create Counter - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF createCounter("counter", $counter) @@ -179,7 +179,7 @@ class StatemachineSubflowErrorHandlingTest : StatemachineErrorHandlingTest() { ENDRULE RULE Throw exception on executeCommitTransaction action after first suspend + commit - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF flagged("subflow_flag") && flagged("suspend_flag") && readCounter("counter") < 5 @@ -257,7 +257,7 @@ class StatemachineSubflowErrorHandlingTest : StatemachineErrorHandlingTest() { val rules = """ RULE Create Counter - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF createCounter("counter", $counter) @@ -273,7 +273,7 @@ class StatemachineSubflowErrorHandlingTest : StatemachineErrorHandlingTest() { ENDRULE RULE Throw exception on executeCommitTransaction action after first suspend + commit - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF flagged("subflow_flag") && readCounter("counter") < 5 @@ -351,7 +351,7 @@ class StatemachineSubflowErrorHandlingTest : StatemachineErrorHandlingTest() { val rules = """ RULE Create Counter - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF createCounter("counter", $counter) @@ -367,7 +367,7 @@ class StatemachineSubflowErrorHandlingTest : StatemachineErrorHandlingTest() { ENDRULE RULE Throw exception on executeCommitTransaction action after first suspend + commit - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF flagged("subflow_flag") && flagged("commit_flag") && readCounter("counter") < 5 @@ -375,7 +375,7 @@ class StatemachineSubflowErrorHandlingTest : StatemachineErrorHandlingTest() { ENDRULE RULE Set flag when executing first commit - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF flagged("subflow_flag") && !flagged("commit_flag") diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/ActionExecutorImpl.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/ActionExecutorImpl.kt index c3ddadd716..fffecff638 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/ActionExecutorImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/ActionExecutorImpl.kt @@ -19,7 +19,7 @@ import java.time.Duration /** * This is the bottom execution engine of flow side-effects. */ -class ActionExecutorImpl( +internal class ActionExecutorImpl( private val services: ServiceHubInternal, private val checkpointStorage: CheckpointStorage, private val flowMessaging: FlowMessaging, diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowSleepScheduler.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowSleepScheduler.kt index 4ff9df43c9..63fcd5c6e8 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowSleepScheduler.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowSleepScheduler.kt @@ -9,7 +9,7 @@ import java.util.concurrent.ScheduledExecutorService import java.util.concurrent.ScheduledFuture import java.util.concurrent.TimeUnit -internal class FlowSleepScheduler(private val smm: StateMachineManagerInternal, private val scheduledExecutor: ScheduledExecutorService) { +internal class FlowSleepScheduler(private val innerState: StateMachineInnerState, private val scheduledExecutor: ScheduledExecutorService) { private companion object { val log = contextLogger() @@ -31,15 +31,6 @@ internal class FlowSleepScheduler(private val smm: StateMachineManagerInternal, currentState.future = setAlarmClock(fiber, duration) } - /** - * Schedule a wake up event. - * - * @param fiber The [FlowFiber] to schedule a wake up event for - */ - fun scheduleWakeUp(fiber: FlowFiber) { - fiber.scheduleEvent(Event.WakeUpFromSleep) - } - /** * Cancel a sleeping flow's future. Note, this does not cause the flow to wake up. * @@ -64,15 +55,24 @@ internal class FlowSleepScheduler(private val smm: StateMachineManagerInternal, private fun setAlarmClock(fiber: FlowFiber, duration: Duration): ScheduledFuture { val instance = fiber.instanceId - log.debug { "Putting flow to sleep for $duration" } + log.debug { "Putting flow ${instance.runId} to sleep for $duration" } return scheduledExecutor.schedule( { log.debug { "Scheduling flow wake up event for flow ${instance.runId}" } - // This passes back into the SMM to check that the fiber that went to sleep is the same fiber that is now being scheduled - // with the wake up event - smm.scheduleFlowWakeUp(instance) + scheduleWakeUp(instance) }, duration.toMillis(), TimeUnit.MILLISECONDS ) } + + private fun scheduleWakeUp(instance: StateMachineInstanceId) { + innerState.withLock { + flows[instance.runId]?.let { flow -> + // Only schedule a wake up event if the fiber the flow is executing on has not changed + if (flow.fiber.instanceId == instance) { + flow.fiber.scheduleEvent(Event.WakeUpFromSleep) + } + } + } + } } \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowTimeoutScheduler.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowTimeoutScheduler.kt new file mode 100644 index 0000000000..5ba6d700e3 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowTimeoutScheduler.kt @@ -0,0 +1,101 @@ +package net.corda.node.services.statemachine + +import net.corda.core.flows.StateMachineRunId +import net.corda.core.utilities.contextLogger +import net.corda.core.utilities.debug +import net.corda.node.services.api.ServiceHubInternal +import java.util.concurrent.Future +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.ScheduledFuture +import java.util.concurrent.TimeUnit + +internal class FlowTimeoutScheduler( + private val innerState: StateMachineInnerState, + private val scheduledExecutor: ScheduledExecutorService, + private val serviceHub: ServiceHubInternal +) { + + private companion object { + val log = contextLogger() + } + + /** + * Schedules the flow [flowId] to be retried if it does not finish within the timeout period + * specified in the config. + * + * @param flowId The id of the flow that the timeout is scheduled for + */ + fun timeout(flowId: StateMachineRunId) { + timeout(flowId) { flow, retryCount -> + val scheduledFuture = scheduleTimeoutException(flow, calculateDefaultTimeoutSeconds(retryCount)) + ScheduledTimeout(scheduledFuture, retryCount + 1) + } + } + + /** + * Cancel a flow's timeout future. + * + * @param flowId The flow's id + */ + fun cancel(flowId: StateMachineRunId) { + innerState.withLock { + timedFlows[flowId]?.let { (future, _) -> + future.cancelIfRunning() + timedFlows.remove(flowId) + } + } + } + + /** + * Resets a flow's timeout with the input timeout duration, only if it is longer than the default flow timeout configuration. + * + * @param flowId The flow's id + * @param timeoutSeconds The custom timeout + */ + fun resetCustomTimeout(flowId: StateMachineRunId, timeoutSeconds: Long) { + if (timeoutSeconds < serviceHub.configuration.flowTimeout.timeout.seconds) { + log.debug { "Ignoring request to set time-out on timed flow $flowId to $timeoutSeconds seconds which is shorter than default of ${serviceHub.configuration.flowTimeout.timeout.seconds} seconds." } + return + } + log.debug { "Processing request to set time-out on timed flow $flowId to $timeoutSeconds seconds." } + timeout(flowId) { flow, retryCount -> + val scheduledFuture = scheduleTimeoutException(flow, timeoutSeconds) + ScheduledTimeout(scheduledFuture, retryCount) + } + } + + private inline fun timeout(flowId: StateMachineRunId, timeout: (flow: Flow<*>, retryCount: Int) -> ScheduledTimeout) { + innerState.withLock { + val flow = flows[flowId] + if (flow != null) { + val retryCount = timedFlows[flowId]?.let { (future, retryCount) -> + future.cancelIfRunning() + retryCount + } ?: 0 + timedFlows[flowId] = timeout(flow, retryCount) + } else { + log.warn("Unable to schedule timeout for flow $flowId – flow not found.") + } + } + } + + /** Schedules a [FlowTimeoutException] to be fired in order to restart the flow. */ + private fun scheduleTimeoutException(flow: Flow<*>, delay: Long): ScheduledFuture<*> { + return scheduledExecutor.schedule({ + val event = Event.Error(FlowTimeoutException()) + flow.fiber.scheduleEvent(event) + }, delay, TimeUnit.SECONDS) + } + + private fun calculateDefaultTimeoutSeconds(retryCount: Int): Long { + return serviceHub.configuration.flowTimeout.run { + val timeoutDelaySeconds = + timeout.seconds * Math.pow(backoffBase, Integer.min(retryCount, maxRestartCount).toDouble()).toLong() + maxOf(1L, ((1.0 + Math.random()) * timeoutDelaySeconds / 2).toLong()) + } + } + + private fun Future<*>.cancelIfRunning() { + if (!isDone) cancel(true) + } +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/SingleThreadedStateMachineManager.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/SingleThreadedStateMachineManager.kt index fe395f7483..3c7a7dd27f 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/SingleThreadedStateMachineManager.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/SingleThreadedStateMachineManager.kt @@ -13,10 +13,8 @@ import net.corda.core.flows.FlowLogic import net.corda.core.flows.StateMachineRunId import net.corda.core.identity.Party import net.corda.core.internal.FlowStateMachine -import net.corda.core.internal.ThreadBox import net.corda.core.internal.bufferUntilSubscribed import net.corda.core.internal.castIfPossible -import net.corda.core.internal.concurrent.OpenFuture import net.corda.core.internal.concurrent.map import net.corda.core.internal.concurrent.mapError import net.corda.core.internal.concurrent.openFuture @@ -49,8 +47,6 @@ import net.corda.serialization.internal.CheckpointSerializeAsTokenContextImpl import net.corda.serialization.internal.withTokenContext import org.apache.activemq.artemis.utils.ReusableLatch import rx.Observable -import rx.subjects.PublishSubject -import java.lang.Integer.min import java.security.SecureRandom import java.time.Duration import java.util.HashSet @@ -58,8 +54,6 @@ import java.util.* import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ExecutorService import java.util.concurrent.Executors -import java.util.concurrent.ScheduledFuture -import java.util.concurrent.TimeUnit import javax.annotation.concurrent.ThreadSafe import kotlin.collections.component1 import kotlin.collections.component2 @@ -71,7 +65,7 @@ import kotlin.streams.toList * thread actually starts them via [deliverExternalEvent]. */ @ThreadSafe -class SingleThreadedStateMachineManager( +internal class SingleThreadedStateMachineManager( val serviceHub: ServiceHubInternal, private val checkpointStorage: CheckpointStorage, val executor: ExecutorService, @@ -84,27 +78,7 @@ class SingleThreadedStateMachineManager( private val logger = contextLogger() } - private data class ScheduledTimeout( - /** Will fire a [FlowTimeoutException] indicating to the flow hospital to restart the flow. */ - val scheduledFuture: ScheduledFuture<*>, - /** Specifies the number of times this flow has been retried. */ - val retryCount: Int = 0 - ) - - // A list of all the state machines being managed by this class. We expose snapshots of it via the stateMachines - // property. - private class InnerState { - val changesPublisher = PublishSubject.create()!! - /** True if we're shutting down, so don't resume anything. */ - var stopping = false - val flows = HashMap>() - val pausedFlows = HashMap() - val startedFutures = HashMap>() - /** Flows scheduled to be retried if not finished within the specified timeout period. */ - val timedFlows = HashMap() - } - - private val mutex = ThreadBox(InnerState()) + private val innerState = StateMachineInnerStateImpl() private val scheduler = FiberExecutorScheduler("Same thread scheduler", executor) private val scheduledFutureExecutor = Executors.newSingleThreadScheduledExecutor( ThreadFactoryBuilder().setNameFormat("flow-scheduled-future-thread").setDaemon(true).build() @@ -115,7 +89,8 @@ class SingleThreadedStateMachineManager( private val metrics = serviceHub.monitoringService.metrics private val sessionToFlow = ConcurrentHashMap() private val flowMessaging: FlowMessaging = FlowMessagingImpl(serviceHub) - private val flowSleepScheduler = FlowSleepScheduler(this, scheduledFutureExecutor) + private val flowSleepScheduler = FlowSleepScheduler(innerState, scheduledFutureExecutor) + private val flowTimeoutScheduler = FlowTimeoutScheduler(innerState, scheduledFutureExecutor, serviceHub) private val fiberDeserializationChecker = if (serviceHub.configuration.shouldCheckCheckpoints()) FiberDeserializationChecker() else null private val ourSenderUUID = serviceHub.networkService.ourSenderUUID @@ -126,7 +101,7 @@ class SingleThreadedStateMachineManager( private val transitionExecutor = makeTransitionExecutor() override val allStateMachines: List> - get() = mutex.locked { flows.values.map { it.fiber.logic } } + get() = innerState.withLock { flows.values.map { it.fiber.logic } } private val totalStartedFlows = metrics.counter("Flows.Started") private val totalFinishedFlows = metrics.counter("Flows.Finished") @@ -137,7 +112,7 @@ class SingleThreadedStateMachineManager( * * We use assignment here so that multiple subscribers share the same wrapped Observable. */ - override val changes: Observable = mutex.content.changesPublisher + override val changes: Observable = innerState.changesPublisher override fun start(tokenizableServices: List, startMode: StateMachineManager.StartMode): CordaFuture { checkQuasarJavaAgentPresence() @@ -157,19 +132,20 @@ class SingleThreadedStateMachineManager( StateMachineManager.StartMode.Safe -> markAllFlowsAsPaused() } this.flowCreator = FlowCreator( - checkpointSerializationContext, - checkpointStorage, - scheduler, - database, - transitionExecutor, - actionExecutor, - secureRandom, - serviceHub, - unfinishedFibers, - ::resetCustomTimeout) + checkpointSerializationContext, + checkpointStorage, + scheduler, + database, + transitionExecutor, + actionExecutor, + secureRandom, + serviceHub, + unfinishedFibers, + flowTimeoutScheduler::resetCustomTimeout + ) val fibers = restoreFlowsFromCheckpoints() - metrics.register("Flows.InFlight", Gauge { mutex.content.flows.size }) + metrics.register("Flows.InFlight", Gauge { innerState.flows.size }) Fiber.setDefaultUncaughtExceptionHandler { fiber, throwable -> if (throwable is VirtualMachineError) { errorAndTerminate("Caught unrecoverable error from flow. Forcibly terminating the JVM, this might leave resources open, and most likely will.", throwable) @@ -179,7 +155,7 @@ class SingleThreadedStateMachineManager( } val pausedFlows = restoreNonResidentFlowsFromPausedCheckpoints() - mutex.locked { + innerState.withLock { this.pausedFlows.putAll(pausedFlows) for ((id, flow) in pausedFlows) { val checkpoint = flow.checkpoint @@ -199,10 +175,10 @@ class SingleThreadedStateMachineManager( } } - override fun snapshot(): Set> = mutex.content.flows.values.map { it.fiber }.toSet() + override fun snapshot(): Set> = innerState.flows.values.map { it.fiber }.toSet() override fun > findStateMachines(flowClass: Class): List>> { - return mutex.locked { + return innerState.withLock { flows.values.mapNotNull { flowClass.castIfPossible(it.fiber.logic)?.let { it to it.stateMachine.resultFuture } } @@ -217,7 +193,7 @@ class SingleThreadedStateMachineManager( */ override fun stop(allowedUnsuspendedFiberCount: Int) { require(allowedUnsuspendedFiberCount >= 0){"allowedUnsuspendedFiberCount must be greater than or equal to zero"} - mutex.locked { + innerState.withLock { if (stopping) throw IllegalStateException("Already stopping!") stopping = true for ((_, flow) in flows) { @@ -241,7 +217,7 @@ class SingleThreadedStateMachineManager( * calls to [allStateMachines] */ override fun track(): DataFeed>, StateMachineManager.Change> { - return mutex.locked { + return innerState.withMutex { database.transaction { DataFeed(flows.values.map { it.fiber.logic }, changesPublisher.bufferUntilSubscribed().wrapWithDatabaseTransaction(database)) } @@ -266,7 +242,7 @@ class SingleThreadedStateMachineManager( } override fun killFlow(id: StateMachineRunId): Boolean { - val killFlowResult = mutex.locked { + val killFlowResult = innerState.withLock { val flow = flows[id] if (flow != null) { logger.info("Killing flow $id known to this node.") @@ -281,7 +257,7 @@ class SingleThreadedStateMachineManager( unfinishedFibers.countDown() val state = flow.fiber.transientState - return@locked if (state != null) { + return@withLock if (state != null) { state.value.isKilled = true flow.fiber.scheduleEvent(Event.DoRemainingWork) true @@ -333,9 +309,9 @@ class SingleThreadedStateMachineManager( } override fun removeFlow(flowId: StateMachineRunId, removalReason: FlowRemovalReason, lastState: StateMachineState) { - mutex.locked { - cancelTimeoutIfScheduled(flowId) - cancelFlowSleep(lastState) + innerState.withLock { + flowTimeoutScheduler.cancel(flowId) + flowSleepScheduler.cancel(lastState) val flow = flows.remove(flowId) if (flow != null) { decrementLiveFibers() @@ -352,7 +328,7 @@ class SingleThreadedStateMachineManager( } override fun signalFlowHasStarted(flowId: StateMachineRunId) { - mutex.locked { + innerState.withLock { startedFutures.remove(flowId)?.set(Unit) flows[flowId]?.let { flow -> changesPublisher.onNext(StateMachineManager.Change.Add(flow.fiber.logic)) @@ -378,7 +354,7 @@ class SingleThreadedStateMachineManager( return checkpointStorage.getCheckpointsToRun().use { it.mapNotNull { (id, serializedCheckpoint) -> // If a flow is added before start() then don't attempt to restore it - mutex.locked { if (id in flows) return@mapNotNull null } + innerState.withLock { if (id in flows) return@mapNotNull null } val checkpoint = tryDeserializeCheckpoint(serializedCheckpoint, id) ?: return@mapNotNull null flowCreator.createFlowFromCheckpoint(id, checkpoint) }.toList() @@ -403,11 +379,11 @@ class SingleThreadedStateMachineManager( @Suppress("TooGenericExceptionCaught", "ComplexMethod", "MaxLineLength") // this is fully intentional here, see comment in the catch clause override fun retryFlowFromSafePoint(currentState: StateMachineState) { - cancelFlowSleep(currentState) + flowSleepScheduler.cancel(currentState) // Get set of external events val flowId = currentState.flowLogic.runId try { - val oldFlowLeftOver = mutex.locked { flows[flowId] }?.fiber?.transientValues?.value?.eventQueue + val oldFlowLeftOver = innerState.withLock { flows[flowId] }?.fiber?.transientValues?.value?.eventQueue if (oldFlowLeftOver == null) { logger.error("Unable to find flow for flow $flowId. Something is very wrong. The flow will not retry.") return @@ -428,7 +404,7 @@ class SingleThreadedStateMachineManager( // Just flow initiation message null } - mutex.locked { + innerState.withLock { if (stopping) { return } @@ -467,7 +443,7 @@ class SingleThreadedStateMachineManager( } override fun deliverExternalEvent(event: ExternalEvent) { - mutex.locked { + innerState.withLock { if (!stopping) { when (event) { is ExternalEvent.ExternalMessageEvent -> onSessionMessage(event) @@ -527,7 +503,7 @@ class SingleThreadedStateMachineManager( } } else { val event = Event.DeliverSessionMessage(sessionMessage, deduplicationHandler, sender) - mutex.locked { + 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) } @@ -623,7 +599,7 @@ class SingleThreadedStateMachineManager( deduplicationHandler: DeduplicationHandler? ): CordaFuture> { - val existingFlow = mutex.locked { flows[flowId] } + val existingFlow = innerState.withLock { flows[flowId] } val existingCheckpoint = if (existingFlow != null && existingFlow.fiber.transientState?.value?.isAnyCheckpointPersisted == true) { // Load the flow's checkpoint // The checkpoint will be missing if the flow failed before persisting the original checkpoint @@ -648,7 +624,7 @@ class SingleThreadedStateMachineManager( val flow = flowCreator.createFlowFromLogic(flowId, invocationContext, flowLogic, flowStart, ourIdentity, existingCheckpoint, deduplicationHandler, ourSenderUUID) val startedFuture = openFuture() - mutex.locked { + innerState.withLock { startedFutures[flowId] = startedFuture } totalStartedFlows.inc() @@ -657,110 +633,17 @@ class SingleThreadedStateMachineManager( } override fun scheduleFlowTimeout(flowId: StateMachineRunId) { - mutex.locked { scheduleTimeout(flowId) } + flowTimeoutScheduler.timeout(flowId) } override fun cancelFlowTimeout(flowId: StateMachineRunId) { - mutex.locked { cancelTimeoutIfScheduled(flowId) } + flowTimeoutScheduler.cancel(flowId) } override fun scheduleFlowSleep(fiber: FlowFiber, currentState: StateMachineState, duration: Duration) { flowSleepScheduler.sleep(fiber, currentState, duration) } - override fun scheduleFlowWakeUp(instanceId: StateMachineInstanceId) { - mutex.locked { - flows[instanceId.runId]?.let { flow -> - // Only schedule a wake up event if the fiber the flow is executing on has not changed - if (flow.fiber.instanceId == instanceId) { - flowSleepScheduler.scheduleWakeUp(flow.fiber) - } - } - } - } - - private fun cancelFlowSleep(currentState: StateMachineState) { - flowSleepScheduler.cancel(currentState) - } - - /** - * Schedules the flow [flowId] to be retried if it does not finish within the timeout period - * specified in the config. - * - * Assumes lock is taken on the [InnerState]. - */ - private fun InnerState.scheduleTimeout(flowId: StateMachineRunId) { - val flow = flows[flowId] - if (flow != null) { - val scheduledTimeout = timedFlows[flowId] - val retryCount = if (scheduledTimeout != null) { - val timeoutFuture = scheduledTimeout.scheduledFuture - if (!timeoutFuture.isDone) scheduledTimeout.scheduledFuture.cancel(true) - scheduledTimeout.retryCount - } else 0 - val scheduledFuture = scheduleTimeoutException(flow, calculateDefaultTimeoutSeconds(retryCount)) - timedFlows[flowId] = ScheduledTimeout(scheduledFuture, retryCount + 1) - } else { - logger.warn("Unable to schedule timeout for flow $flowId – flow not found.") - } - } - - private fun resetCustomTimeout(flowId: StateMachineRunId, timeoutSeconds: Long) { - if (timeoutSeconds < serviceHub.configuration.flowTimeout.timeout.seconds) { - logger.debug { "Ignoring request to set time-out on timed flow $flowId to $timeoutSeconds seconds which is shorter than default of ${serviceHub.configuration.flowTimeout.timeout.seconds} seconds." } - return - } - logger.debug { "Processing request to set time-out on timed flow $flowId to $timeoutSeconds seconds." } - mutex.locked { - resetCustomTimeout(flowId, timeoutSeconds) - } - } - - private fun InnerState.resetCustomTimeout(flowId: StateMachineRunId, timeoutSeconds: Long) { - val flow = flows[flowId] - if (flow != null) { - val scheduledTimeout = timedFlows[flowId] - val retryCount = if (scheduledTimeout != null) { - val timeoutFuture = scheduledTimeout.scheduledFuture - if (!timeoutFuture.isDone) scheduledTimeout.scheduledFuture.cancel(true) - scheduledTimeout.retryCount - } else 0 - val scheduledFuture = scheduleTimeoutException(flow, timeoutSeconds) - timedFlows[flowId] = ScheduledTimeout(scheduledFuture, retryCount) - } else { - logger.warn("Unable to schedule timeout for flow $flowId – flow not found.") - } - } - - /** Schedules a [FlowTimeoutException] to be fired in order to restart the flow. */ - private fun scheduleTimeoutException(flow: Flow<*>, delay: Long): ScheduledFuture<*> { - return with(serviceHub.configuration.flowTimeout) { - scheduledFutureExecutor.schedule({ - val event = Event.Error(FlowTimeoutException()) - flow.fiber.scheduleEvent(event) - }, delay, TimeUnit.SECONDS) - } - } - - private fun calculateDefaultTimeoutSeconds(retryCount: Int): Long { - return with(serviceHub.configuration.flowTimeout) { - val timeoutDelaySeconds = timeout.seconds * Math.pow(backoffBase, min(retryCount, maxRestartCount).toDouble()).toLong() - maxOf(1L, ((1.0 + Math.random()) * timeoutDelaySeconds / 2).toLong()) - } - } - - /** - * Cancels any scheduled flow timeout for [flowId]. - * - * Assumes lock is taken on the [InnerState]. - */ - private fun InnerState.cancelTimeoutIfScheduled(flowId: StateMachineRunId) { - timedFlows[flowId]?.let { (future, _) -> - if (!future.isDone) future.cancel(true) - timedFlows.remove(flowId) - } - } - private fun tryDeserializeCheckpoint(serializedCheckpoint: Checkpoint.Serialized, flowId: StateMachineRunId): Checkpoint? { return try { serializedCheckpoint.deserialize(checkpointSerializationContext!!) @@ -775,7 +658,7 @@ class SingleThreadedStateMachineManager( for (sessionId in getFlowSessionIds(checkpoint)) { sessionToFlow[sessionId] = id } - mutex.locked { + innerState.withLock { if (stopping) { startedFutures[id]?.setException(IllegalStateException("Will not start flow as SMM is stopping")) logger.trace("Not resuming as SMM is stopping.") @@ -788,7 +671,7 @@ class SingleThreadedStateMachineManager( oldFlow.resultFuture.captureLater(flow.resultFuture) } val flowLogic = flow.fiber.logic - if (flowLogic.isEnabledTimedFlow()) scheduleTimeout(id) + if (flowLogic.isEnabledTimedFlow()) flowTimeoutScheduler.timeout(id) flow.fiber.scheduleEvent(Event.DoRemainingWork) startOrResume(checkpoint, flow) } @@ -848,7 +731,7 @@ class SingleThreadedStateMachineManager( return StaffedFlowHospital(flowMessaging, serviceHub.clock, ourSenderUUID) } - private fun InnerState.removeFlowOrderly( + private fun StateMachineInnerState.removeFlowOrderly( flow: Flow<*>, removalReason: FlowRemovalReason.OrderlyFinish, lastState: StateMachineState @@ -864,7 +747,7 @@ class SingleThreadedStateMachineManager( changesPublisher.onNext(StateMachineManager.Change.Removed(lastState.flowLogic, Try.Success(removalReason.flowReturnValue))) } - private fun InnerState.removeFlowError( + private fun StateMachineInnerState.removeFlowError( flow: Flow<*>, removalReason: FlowRemovalReason.ErrorFinish, lastState: StateMachineState diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineInnerState.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineInnerState.kt new file mode 100644 index 0000000000..0252e21e80 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineInnerState.kt @@ -0,0 +1,44 @@ +package net.corda.node.services.statemachine + +import net.corda.core.flows.StateMachineRunId +import net.corda.core.internal.concurrent.OpenFuture +import net.corda.node.services.statemachine.StateMachineManager.Change +import rx.subjects.PublishSubject +import java.util.concurrent.ScheduledFuture +import java.util.concurrent.locks.Lock +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock + +internal interface StateMachineInnerState { + val lock: Lock + val flows: MutableMap> + val pausedFlows: MutableMap + val startedFutures: MutableMap> + val changesPublisher: PublishSubject + /** Flows scheduled to be retried if not finished within the specified timeout period. */ + val timedFlows: MutableMap + + fun withMutex(block: StateMachineInnerState.() -> R): R +} + +internal class StateMachineInnerStateImpl : StateMachineInnerState { + /** True if we're shutting down, so don't resume anything. */ + var stopping = false + override val lock = ReentrantLock() + override val changesPublisher = PublishSubject.create()!! + override val flows = HashMap>() + override val pausedFlows = HashMap() + override val startedFutures = HashMap>() + override val timedFlows = HashMap() + + override fun withMutex(block: StateMachineInnerState.() -> R): R = lock.withLock { block(this) } +} + +internal inline fun T.withLock(block: T.() -> R): R = lock.withLock { block(this) } + +internal data class ScheduledTimeout( + /** Will fire a [FlowTimeoutException] indicating to the flow hospital to restart the flow. */ + val scheduledFuture: ScheduledFuture<*>, + /** Specifies the number of times this flow has been retried. */ + val retryCount: Int = 0 +) \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt index c6aebbdf62..66a5a60797 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt @@ -102,7 +102,7 @@ interface StateMachineManager { // These must be idempotent! A later failure in the state transition may error the flow state, and a replay may call // these functions again -interface StateMachineManagerInternal { +internal interface StateMachineManagerInternal { fun signalFlowHasStarted(flowId: StateMachineRunId) fun addSessionBinding(flowId: StateMachineRunId, sessionId: SessionId) fun removeSessionBindings(sessionIds: Set) @@ -111,7 +111,6 @@ interface StateMachineManagerInternal { fun scheduleFlowTimeout(flowId: StateMachineRunId) fun cancelFlowTimeout(flowId: StateMachineRunId) fun scheduleFlowSleep(fiber: FlowFiber, currentState: StateMachineState, duration: Duration) - fun scheduleFlowWakeUp(instanceId: StateMachineInstanceId) } /** diff --git a/node/src/main/kotlin/net/corda/node/utilities/StateMachineManagerUtils.kt b/node/src/main/kotlin/net/corda/node/utilities/StateMachineManagerUtils.kt index 25ec223836..99aeebc6d8 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/StateMachineManagerUtils.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/StateMachineManagerUtils.kt @@ -14,7 +14,7 @@ import java.lang.reflect.Field * If the new tracker contains any child trackers from sub-flows, we need to attach those to the old tracker as well. */ //TODO: instead of replacing the progress tracker after constructing the flow logic, we should inject it during fiber deserialization -fun StateMachineManagerInternal.injectOldProgressTracker(oldTracker: ProgressTracker?, newFlowLogic: FlowLogic<*>) { +internal fun StateMachineManagerInternal.injectOldProgressTracker(oldTracker: ProgressTracker?, newFlowLogic: FlowLogic<*>) { if (oldTracker != null) { val newTracker = newFlowLogic.progressTracker if (newTracker != null) { From 3f03de6fbd8734a31331db4191e9096f5061d163 Mon Sep 17 00:00:00 2001 From: Denis Rekalov Date: Mon, 29 Jun 2020 09:23:29 +0100 Subject: [PATCH 31/85] CORDA-3856: Add Artemis plugin for validating AMQP message header and type (#6407) --- detekt-baseline.xml | 1 + .../messaging/MQSecurityAsNodeTest.kt | 19 ++++ .../services/messaging/MQSecurityTest.kt | 2 +- .../services/messaging/P2PMQSecurityTest.kt | 2 + .../internal/artemis/UserValidationPlugin.kt | 47 ++++++++++ .../messaging/ArtemisMessagingServer.kt | 2 + .../artemis/UserValidationPluginTest.kt | 89 +++++++++++++++++++ 7 files changed, 161 insertions(+), 1 deletion(-) create mode 100644 node/src/main/kotlin/net/corda/node/internal/artemis/UserValidationPlugin.kt create mode 100644 node/src/test/kotlin/net/corda/node/internal/artemis/UserValidationPluginTest.kt diff --git a/detekt-baseline.xml b/detekt-baseline.xml index 687cece47d..80acbb93f9 100644 --- a/detekt-baseline.xml +++ b/detekt-baseline.xml @@ -1639,6 +1639,7 @@ TooGenericExceptionCaught:TransformTypes.kt$TransformTypes.Companion$e: IndexOutOfBoundsException TooGenericExceptionCaught:TransitionExecutorImpl.kt$TransitionExecutorImpl$exception: Exception TooGenericExceptionCaught:Try.kt$Try.Companion$t: Throwable + TooGenericExceptionCaught:UserValidationPlugin.kt$UserValidationPlugin$e: Throwable TooGenericExceptionCaught:Utils.kt$e: Exception TooGenericExceptionCaught:V1NodeConfigurationSpec.kt$V1NodeConfigurationSpec$e: Exception TooGenericExceptionCaught:ValidatingNotaryFlow.kt$ValidatingNotaryFlow$e: Exception diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsNodeTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsNodeTest.kt index 6f95a7964c..fa5b6950ef 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsNodeTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsNodeTest.kt @@ -1,6 +1,7 @@ package net.corda.services.messaging import net.corda.core.crypto.Crypto +import net.corda.core.crypto.toStringShort import net.corda.core.identity.CordaX500Name import net.corda.core.internal.createDirectories import net.corda.core.internal.exists @@ -14,6 +15,8 @@ import net.corda.nodeapi.internal.crypto.CertificateType import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.loadDevCaTrustStore import net.corda.testing.internal.stubs.CertificateStoreStubs +import net.corda.nodeapi.internal.ArtemisMessagingComponent +import net.corda.testing.core.singleIdentity import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration import org.apache.activemq.artemis.api.core.ActiveMQClusterSecurityException import org.apache.activemq.artemis.api.core.ActiveMQNotConnectedException @@ -22,8 +25,10 @@ import org.assertj.core.api.Assertions.assertThatExceptionOfType import org.bouncycastle.asn1.x509.GeneralName import org.bouncycastle.asn1.x509.GeneralSubtree import org.bouncycastle.asn1.x509.NameConstraints +import org.junit.Ignore import org.junit.Test import java.nio.file.Files +import kotlin.test.assertEquals /** * Runs the security tests with the attacker pretending to be a node on the network. @@ -37,6 +42,7 @@ class MQSecurityAsNodeTest : P2PMQSecurityTest() { attacker.start(PEER_USER, PEER_USER) // Login as a peer } + @Ignore("Core protocol messages are no allowed for PEER_USER: need to switch to AMQP") @Test(timeout=300_000) fun `send message to RPC requests address`() { assertSendAttackFails(RPCApi.RPC_SERVER_QUEUE_NAME) @@ -117,4 +123,17 @@ class MQSecurityAsNodeTest : P2PMQSecurityTest() { attacker.start(PEER_USER, PEER_USER) } } + + @Ignore("Core protocol messages are no allowed for PEER_USER: need to switch to AMQP") + override fun `send message to notifications address`() { + } + + @Test(timeout=300_000) + fun `send message on core protocol`() { + val message = attacker.createMessage() + assertEquals(true, attacker.producer.isBlockOnNonDurableSend) + assertThatExceptionOfType(ActiveMQSecurityException::class.java).isThrownBy { + attacker.producer.send("${ArtemisMessagingComponent.P2P_PREFIX}${alice.info.singleIdentity().owningKey.toStringShort()}", message) + }.withMessageContaining("CoreMessage").withMessageContaining("AMQPMessage") + } } diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt index 92e8c72287..50e5677009 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt @@ -79,7 +79,7 @@ abstract class MQSecurityTest : NodeBasedTest() { } @Test(timeout=300_000) - fun `send message to notifications address`() { + open fun `send message to notifications address`() { assertSendAttackFails(NOTIFICATIONS_ADDRESS) } diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMQSecurityTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMQSecurityTest.kt index 240ad1007d..068c521a6d 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMQSecurityTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMQSecurityTest.kt @@ -7,6 +7,7 @@ import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_PREFIX import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEERS_PREFIX import net.corda.testing.core.BOB_NAME import net.corda.testing.core.singleIdentity +import org.junit.Ignore import org.junit.Test /** @@ -25,6 +26,7 @@ abstract class P2PMQSecurityTest : MQSecurityTest() { assertConsumeAttackFails("$PEERS_PREFIX${bobParty.owningKey.toStringShort()}") } + @Ignore("Core protocol messages are no allowed for PEER_USER: need to switch to AMQP") @Test(timeout=300_000) fun `send message to address of peer which has been communicated with`() { val bobParty = startBobAndCommunicateWithAlice() diff --git a/node/src/main/kotlin/net/corda/node/internal/artemis/UserValidationPlugin.kt b/node/src/main/kotlin/net/corda/node/internal/artemis/UserValidationPlugin.kt new file mode 100644 index 0000000000..963f5169a6 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/artemis/UserValidationPlugin.kt @@ -0,0 +1,47 @@ +package net.corda.node.internal.artemis + +import net.corda.core.utilities.contextLogger +import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEER_USER +import org.apache.activemq.artemis.api.core.ActiveMQSecurityException +import org.apache.activemq.artemis.api.core.Message +import org.apache.activemq.artemis.core.server.ServerSession +import org.apache.activemq.artemis.core.server.plugin.ActiveMQServerPlugin +import org.apache.activemq.artemis.core.transaction.Transaction +import org.apache.activemq.artemis.protocol.amqp.broker.AMQPMessage + +/** + * Plugin to verify the user in the AMQP message header against the user in the authenticated session. + * + * In core protocol, Artemis Server automatically overwrites the _AMQ_VALIDATED_USER field in message header according to authentication + * of the session. However, this is not done for AMQP protocol, which is used by Corda. Hence, _AMQ_VALIDATED_USER in AMQP packet is + * delivered in the same form, as it was produced by counterpart. To prevent manipulations of this field by other peers, we should check + * message header against user in authenticated session. + * + * Note that AMQP message is immutable, so changing the header means rebuilding the whole message, which is expensive. Instead, the + * preferred option is to throw an exception. + */ +class UserValidationPlugin : ActiveMQServerPlugin { + companion object { + private val log = contextLogger() + } + + override fun beforeSend(session: ServerSession, tx: Transaction?, message: Message, direct: Boolean, noAutoCreateQueue: Boolean) { + try { + if (session.username == PEER_USER) { + if (message !is AMQPMessage) { + throw ActiveMQSecurityException("Invalid message type: expected [${AMQPMessage::class.java.name}], got [${message.javaClass.name}]") + } + val user = message.getStringProperty(Message.HDR_VALIDATED_USER) + if (user != null && user != session.validatedUser) { + throw ActiveMQSecurityException("_AMQ_VALIDATED_USER mismatch: expected [${session.validatedUser}], got [${user}]") + } + } + } catch (e: ActiveMQSecurityException) { + throw e + } catch (e: Throwable) { + // Artemis swallows any exception except ActiveMQException + log.error("Message validation failed", e) + throw ActiveMQSecurityException("Message validation failed") + } + } +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt index 47965ce37f..e97b2a58d4 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt @@ -142,6 +142,8 @@ class ArtemisMessagingServer(private val config: NodeConfiguration, isJMXManagementEnabled = true isJMXUseBrokerName = true } + // Validate user in AMQP message header against authenticated session + registerBrokerPlugin(UserValidationPlugin()) }.configureAddressSecurity() diff --git a/node/src/test/kotlin/net/corda/node/internal/artemis/UserValidationPluginTest.kt b/node/src/test/kotlin/net/corda/node/internal/artemis/UserValidationPluginTest.kt new file mode 100644 index 0000000000..83f8aae624 --- /dev/null +++ b/node/src/test/kotlin/net/corda/node/internal/artemis/UserValidationPluginTest.kt @@ -0,0 +1,89 @@ +package net.corda.node.internal.artemis + +import com.nhaarman.mockito_kotlin.doReturn +import com.nhaarman.mockito_kotlin.whenever +import net.corda.nodeapi.internal.ArtemisMessagingComponent +import net.corda.testing.core.ALICE_NAME +import net.corda.testing.core.BOB_NAME +import net.corda.testing.internal.rigorousMock +import org.apache.activemq.artemis.api.core.ActiveMQSecurityException +import org.apache.activemq.artemis.api.core.SimpleString +import org.apache.activemq.artemis.core.client.impl.ClientMessageImpl +import org.apache.activemq.artemis.core.server.ServerSession +import org.apache.activemq.artemis.protocol.amqp.broker.AMQPMessage +import org.apache.activemq.artemis.protocol.amqp.converter.AMQPConverter +import org.assertj.core.api.Assertions +import org.junit.Test + +class UserValidationPluginTest { + private val plugin = UserValidationPlugin() + private val coreMessage = ClientMessageImpl(0, false, 0, System.currentTimeMillis(), 4.toByte(), 1024) + private val amqpMessage get() = AMQPConverter.getInstance().fromCore(coreMessage) + private val session = rigorousMock().also { + doReturn(ArtemisMessagingComponent.PEER_USER).whenever(it).username + doReturn(ALICE_NAME.toString()).whenever(it).validatedUser + } + + @Test(timeout = 300_000) + fun `accept AMQP message without user`() { + plugin.beforeSend(session, rigorousMock(), amqpMessage, direct = false, noAutoCreateQueue = false) + } + + @Test(timeout = 300_000) + fun `accept AMQP message with user`() { + coreMessage.putStringProperty("_AMQ_VALIDATED_USER", ALICE_NAME.toString()) + plugin.beforeSend(session, rigorousMock(), amqpMessage, direct = false, noAutoCreateQueue = false) + } + + @Test(timeout = 300_000) + fun `reject AMQP message with different user`() { + coreMessage.putStringProperty("_AMQ_VALIDATED_USER", BOB_NAME.toString()) + Assertions.assertThatExceptionOfType(ActiveMQSecurityException::class.java).isThrownBy { + plugin.beforeSend(session, rigorousMock(), amqpMessage, direct = false, noAutoCreateQueue = false) + }.withMessageContaining("_AMQ_VALIDATED_USER") + } + + @Test(timeout = 300_000) + fun `accept AMQP message with different user on internal session`() { + val internalSession = rigorousMock().also { + doReturn(ArtemisMessagingComponent.NODE_P2P_USER).whenever(it).username + doReturn(ALICE_NAME.toString()).whenever(it).validatedUser + } + coreMessage.putStringProperty("_AMQ_VALIDATED_USER", BOB_NAME.toString()) + plugin.beforeSend(internalSession, rigorousMock(), amqpMessage, direct = false, noAutoCreateQueue = false) + } + + @Test(timeout = 300_000) + fun `reject core message`() { + Assertions.assertThatExceptionOfType(ActiveMQSecurityException::class.java).isThrownBy { + plugin.beforeSend(session, rigorousMock(), coreMessage, direct = false, noAutoCreateQueue = false) + }.withMessageContaining("message type") + } + + @Test(timeout = 300_000) + fun `reject message with exception`() { + coreMessage.putStringProperty("_AMQ_VALIDATED_USER", BOB_NAME.toString()) + val messageWithException = object : AMQPMessage(0, amqpMessage.buffer.array(), null) { + override fun getStringProperty(key: SimpleString?): String { + throw IllegalStateException("My exception") + } + } + // Artemis swallows all exceptions except ActiveMQException, so making sure that proper exception is thrown + Assertions.assertThatExceptionOfType(ActiveMQSecurityException::class.java).isThrownBy { + plugin.beforeSend(session, rigorousMock(), messageWithException, direct = false, noAutoCreateQueue = false) + }.withMessageContaining("Message validation failed") + } + + @Test(timeout = 300_000) + fun `reject message with security exception`() { + coreMessage.putStringProperty("_AMQ_VALIDATED_USER", BOB_NAME.toString()) + val messageWithException = object : AMQPMessage(0, amqpMessage.buffer.array(), null) { + override fun getStringProperty(key: SimpleString?): String { + throw ActiveMQSecurityException("My security exception") + } + } + Assertions.assertThatExceptionOfType(ActiveMQSecurityException::class.java).isThrownBy { + plugin.beforeSend(session, rigorousMock(), messageWithException, direct = false, noAutoCreateQueue = false) + }.withMessageContaining("My security exception") + } +} \ No newline at end of file From 6fc9f2eacf41347577ad620a34b7276a440a984f Mon Sep 17 00:00:00 2001 From: pnemeth Date: Mon, 29 Jun 2020 09:40:12 +0100 Subject: [PATCH 32/85] EG-2854 Failing Test: net.corda.node.internal.NodeStartupCliTest.--nodeconf using absolute path will not be changed (#6393) * EG-2854 Failing Test: net.corda.node.internal.NodeStartupCliTest.--nodeconf using absolute path will not be changed * fix bug EG-2854 * PR comment --- .../kotlin/net/corda/node/internal/NodeStartupCliTest.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/node/src/test/kotlin/net/corda/node/internal/NodeStartupCliTest.kt b/node/src/test/kotlin/net/corda/node/internal/NodeStartupCliTest.kt index 1ebcbf93fb..5433f96758 100644 --- a/node/src/test/kotlin/net/corda/node/internal/NodeStartupCliTest.kt +++ b/node/src/test/kotlin/net/corda/node/internal/NodeStartupCliTest.kt @@ -22,11 +22,13 @@ class NodeStartupCliTest { companion object { private lateinit var workingDirectory: Path + private lateinit var rootDirectory: Path private var customNodeConf = "custom_node.conf" @BeforeClass @JvmStatic fun initDirectories() { workingDirectory = Paths.get(".").normalize().toAbsolutePath() + rootDirectory = Paths.get("/").normalize().toAbsolutePath() } } @@ -64,8 +66,8 @@ class NodeStartupCliTest { @Test(timeout=300_000) fun `--nodeconf using absolute path will not be changed`() { - CommandLine.populateCommand(startup, CommonCliConstants.CONFIG_FILE, "/$customNodeConf") - Assertions.assertThat(startup.cmdLineOptions.configFile).isEqualTo( "/" / customNodeConf) + CommandLine.populateCommand(startup, CommonCliConstants.CONFIG_FILE, (rootDirectory / customNodeConf).toString()) + Assertions.assertThat(startup.cmdLineOptions.configFile).isEqualTo(rootDirectory / customNodeConf) } @Test(timeout=3_000) From 4391585fa9a54d2dceee3c0d06e0fe485abe916f Mon Sep 17 00:00:00 2001 From: Viktor Kolomeyko Date: Mon, 29 Jun 2020 12:24:03 +0100 Subject: [PATCH 33/85] CORDA-3868: Use specific exception to determine SSL handshake timeout (#6409) --- .../nodeapi/internal/protonwrapper/netty/AMQPChannelHandler.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPChannelHandler.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPChannelHandler.kt index c26aa74ada..5ce3db919c 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPChannelHandler.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPChannelHandler.kt @@ -10,6 +10,7 @@ import io.netty.handler.proxy.ProxyConnectionEvent import io.netty.handler.ssl.SniCompletionEvent import io.netty.handler.ssl.SslHandler import io.netty.handler.ssl.SslHandshakeCompletionEvent +import io.netty.handler.ssl.SslHandshakeTimeoutException import io.netty.util.ReferenceCountUtil import net.corda.core.identity.CordaX500Name import net.corda.core.utilities.contextLogger @@ -295,8 +296,8 @@ internal class AMQPChannelHandler(private val serverMode: Boolean, // This happens when the peer node is closed during SSL establishment. when { cause is ClosedChannelException -> logWarnWithMDC("SSL Handshake closed early.") + cause is SslHandshakeTimeoutException -> logWarnWithMDC("SSL Handshake timed out") // Sadly the exception thrown by Netty wrapper requires that we check the message. - cause is SSLException && cause.message == "handshake timed out" -> logWarnWithMDC("SSL Handshake timed out") cause is SSLException && (cause.message?.contains("close_notify") == true) -> logWarnWithMDC("Received close_notify during handshake") // io.netty.handler.ssl.SslHandler.setHandshakeFailureTransportFailure() From eba7fabe5e7e493b5d36262d149a712f00eece8d Mon Sep 17 00:00:00 2001 From: Yash Nabar Date: Mon, 29 Jun 2020 14:34:53 +0100 Subject: [PATCH 34/85] EG-2647: Add persistent Artemis volume (#6389) --- docker/src/docker/Dockerfile | 4 ++++ docker/src/docker/Dockerfile11 | 4 ++++ docker/src/docker/DockerfileAL | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/docker/src/docker/Dockerfile b/docker/src/docker/Dockerfile index 2100ec59f9..d3d287a750 100644 --- a/docker/src/docker/Dockerfile +++ b/docker/src/docker/Dockerfile @@ -7,6 +7,7 @@ RUN apt-get update && \ rm -rf /var/lib/apt/lists/* && \ mkdir -p /opt/corda/cordapps && \ mkdir -p /opt/corda/persistence && \ + mkdir -p /opt/corda/artemis && \ mkdir -p /opt/corda/certificates && \ mkdir -p /opt/corda/drivers && \ mkdir -p /opt/corda/logs && \ @@ -20,6 +21,7 @@ RUN apt-get update && \ ENV CORDAPPS_FOLDER="/opt/corda/cordapps" \ PERSISTENCE_FOLDER="/opt/corda/persistence" \ + ARTEMIS_FOLDER="/opt/corda/artemis" \ CERTIFICATES_FOLDER="/opt/corda/certificates" \ DRIVERS_FOLDER="/opt/corda/drivers" \ CONFIG_FOLDER="/etc/corda" \ @@ -34,6 +36,8 @@ ENV CORDAPPS_FOLDER="/opt/corda/cordapps" \ VOLUME ["/opt/corda/cordapps"] ##PERSISTENCE FOLDER VOLUME ["/opt/corda/persistence"] +##ARTEMIS FOLDER +VOLUME ["/opt/corda/artemis"] ##CERTS FOLDER VOLUME ["/opt/corda/certificates"] ##OPTIONAL JDBC DRIVERS FOLDER diff --git a/docker/src/docker/Dockerfile11 b/docker/src/docker/Dockerfile11 index dfb5eaa0e3..20b48ddcdc 100644 --- a/docker/src/docker/Dockerfile11 +++ b/docker/src/docker/Dockerfile11 @@ -19,6 +19,7 @@ RUN apt-get update && \ rm -rf /var/lib/apt/lists/* && \ mkdir -p /opt/corda/cordapps && \ mkdir -p /opt/corda/persistence && \ + mkdir -p /opt/corda/artemis && \ mkdir -p /opt/corda/certificates && \ mkdir -p /opt/corda/drivers && \ mkdir -p /opt/corda/logs && \ @@ -36,6 +37,7 @@ RUN apt-get update && \ ENV CORDAPPS_FOLDER="/opt/corda/cordapps" \ PERSISTENCE_FOLDER="/opt/corda/persistence" \ + ARTEMIS_FOLDER="/opt/corda/artemis" \ CERTIFICATES_FOLDER="/opt/corda/certificates" \ DRIVERS_FOLDER="/opt/corda/drivers" \ CONFIG_FOLDER="/etc/corda" \ @@ -50,6 +52,8 @@ ENV CORDAPPS_FOLDER="/opt/corda/cordapps" \ VOLUME ["/opt/corda/cordapps"] ##PERSISTENCE FOLDER VOLUME ["/opt/corda/persistence"] +##ARTEMIS FOLDER +VOLUME ["/opt/corda/artemis"] ##CERTS FOLDER VOLUME ["/opt/corda/certificates"] ##OPTIONAL JDBC DRIVERS FOLDER diff --git a/docker/src/docker/DockerfileAL b/docker/src/docker/DockerfileAL index b5ce21ab00..f3c8496604 100644 --- a/docker/src/docker/DockerfileAL +++ b/docker/src/docker/DockerfileAL @@ -10,6 +10,7 @@ RUN amazon-linux-extras enable corretto8 && \ rm -rf /var/cache/yum && \ mkdir -p /opt/corda/cordapps && \ mkdir -p /opt/corda/persistence && \ + mkdir -p /opt/corda/artemis && \ mkdir -p /opt/corda/certificates && \ mkdir -p /opt/corda/drivers && \ mkdir -p /opt/corda/logs && \ @@ -23,6 +24,7 @@ RUN amazon-linux-extras enable corretto8 && \ ENV CORDAPPS_FOLDER="/opt/corda/cordapps" \ PERSISTENCE_FOLDER="/opt/corda/persistence" \ + ARTEMIS_FOLDER="/opt/corda/artemis" \ CERTIFICATES_FOLDER="/opt/corda/certificates" \ DRIVERS_FOLDER="/opt/corda/drivers" \ CONFIG_FOLDER="/etc/corda" \ @@ -37,6 +39,8 @@ ENV CORDAPPS_FOLDER="/opt/corda/cordapps" \ VOLUME ["/opt/corda/cordapps"] ##PERSISTENCE FOLDER VOLUME ["/opt/corda/persistence"] +##ARTEMIS FOLDER +VOLUME ["/opt/corda/artemis"] ##CERTS FOLDER VOLUME ["/opt/corda/certificates"] ##OPTIONAL JDBC DRIVERS FOLDER From 0ddc430b4d2ffb3e6f1f5ba4808b3b6aa6e6bf83 Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Mon, 29 Jun 2020 20:38:51 +0100 Subject: [PATCH 35/85] INRFA-415: Publish empty source and JavaDoc for deterministic modules (#6406) Create empty source and javadoc artifacts for the deterministic modules so that they can be published to Maven Central. --- core-deterministic/README.md | 2 ++ core-deterministic/build.gradle | 12 ++++++++++-- serialization-deterministic/README.md | 2 ++ serialization-deterministic/build.gradle | 12 ++++++++++-- 4 files changed, 24 insertions(+), 4 deletions(-) create mode 100644 core-deterministic/README.md create mode 100644 serialization-deterministic/README.md diff --git a/core-deterministic/README.md b/core-deterministic/README.md new file mode 100644 index 0000000000..766d178882 --- /dev/null +++ b/core-deterministic/README.md @@ -0,0 +1,2 @@ +## corda-core-deterministic. +This artifact is a deterministic subset of the binary contents of `corda-core`. diff --git a/core-deterministic/build.gradle b/core-deterministic/build.gradle index 636ce86800..7b90dfdd82 100644 --- a/core-deterministic/build.gradle +++ b/core-deterministic/build.gradle @@ -207,10 +207,18 @@ artifacts { publish file: deterministicJar, name: jarBaseName, type: 'jar', extension: 'jar', builtBy: metafix } +tasks.named('sourceJar', Jar) { + from 'README.md' + include 'README.md' +} + +tasks.named('javadocJar', Jar) { + from 'README.md' + include 'README.md' +} + publish { dependenciesFrom configurations.deterministicArtifacts - publishSources = false - publishJavadoc = false name jarBaseName } diff --git a/serialization-deterministic/README.md b/serialization-deterministic/README.md new file mode 100644 index 0000000000..abd4a19f0c --- /dev/null +++ b/serialization-deterministic/README.md @@ -0,0 +1,2 @@ +## corda-serialization-deterministic. +This artifact is a deterministic subset of the binary contents of `corda-serialization`. diff --git a/serialization-deterministic/build.gradle b/serialization-deterministic/build.gradle index 773522460d..7ea284daeb 100644 --- a/serialization-deterministic/build.gradle +++ b/serialization-deterministic/build.gradle @@ -193,12 +193,20 @@ artifacts { publish file: deterministicJar, name: jarBaseName, type: 'jar', extension: 'jar', builtBy: metafix } +tasks.named('sourceJar', Jar) { + from 'README.md' + include 'README.md' +} + +tasks.named('javadocJar', Jar) { + from 'README.md' + include 'README.md' +} + publish { dependenciesFrom(configurations.deterministicArtifacts) { defaultScope = 'compile' } - publishSources = false - publishJavadoc = false name jarBaseName } From 18cd24c2b4c73b72366003be9bf3ed7ea5945e1f Mon Sep 17 00:00:00 2001 From: Tamas Veingartner Date: Tue, 30 Jun 2020 10:12:31 +0100 Subject: [PATCH 36/85] NOTICK Ignore pagination large volume test as its completion is environment dependent and might cause issues in slower environments (#6420) --- .../kotlin/net/corda/node/services/vault/VaultQueryTests.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt index f66e903bc4..0eeb7939fe 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt @@ -501,7 +501,8 @@ abstract class VaultQueryTestsBase : VaultQueryParties { assertThat(queriedStates).containsExactlyElementsOf(allStates) } } - + + @Ignore @Test(timeout=300_000) fun `query with sort criteria and pagination on large volume of states should complete in time`() { val numberOfStates = 1000 From f6b57372774c1f49298eb8997b42cee475f5c975 Mon Sep 17 00:00:00 2001 From: Dan Newton Date: Tue, 30 Jun 2020 11:50:42 +0100 Subject: [PATCH 37/85] ENT-5196 handle errors during flow initialisation (#6378) ## Summary This change deals with multiple issues: * Errors that occur during flow initialisation. * Errors that occur when handling the outcome of an existing flow error. * Failures to rollback and close a database transaction when an error occurs in `TransitionExecutorImpl`. * Removal of create and commit transaction actions around retrying a flow. ## Errors that occur during flow initialisation Flow initialisation has been moved into the try/catch that exists inside `FlowStateMachineImpl.run`. This means if an error is thrown all the way out of `initialiseFlow` (which should rarely happen) it will be caught and move into a flow's standard error handling path. The flow should then properly terminate. `Event.Error` was changed to make the choice to rollback be optional. Errors during flow initialisation cause the flow to not have a open database transaction. Therefore there is no need to rollback. ## Errors that occur when handling the outcome of an existing flow error When an error occurs a flow goes to the flow hospital and is given an outcome event to address the original error. If the transition that was processing the error outcome event (`StartErrorPropagation` and `RetryFlowFromSafePoint`) has an error then the flow aborts and nothing happens. This means that the flow is left in a runnable state. To resolve this, we now retry the original error outcome event whenever another error occurs doing so. This is done by adding a new staff member that looks for `ErrorStateTransitionException` thrown in the error code path of `TransitionExecutorImpl`. It then takes the last outcome for that flow and schedules it to run again. This scheduling runs with a backoff. This means that a flow will continually retry the original error outcome event until it completes it successfully. ## Failures to rollback and close a database transaction when an error occurs in `TransitionExecutorImpl` Rolling back and closing the database transaction inside of `TransitionExecutorImpl` is now done inside individual try/catch blocks as this should not prevent the flow from continuing. ## Removal of create and commit transaction actions around retrying a flow The database commit that occurs after retrying a flow can fail which required some custom code just for that event to prevent inconsistent behaviour. The transaction was only needed for reading checkpoints from the database, therefore the transaction was moved into `retryFlowFromSafePoint` instead and the commit removed. If we need to commit data inside of `retryFlowFromSafePoint` in the future, a commit should be added directly to `retryFlowFromSafePoint`. The commit should occur before the flow is started on a new fiber. --- .../persistence/DatabaseTransaction.kt | 20 +- ...st.kt => StateMachineErrorHandlingTest.kt} | 132 +- ... StateMachineFinalityErrorHandlingTest.kt} | 172 +-- .../StateMachineFlowInitErrorHandlingTest.kt | 447 ++++++ .../StateMachineGeneralErrorHandlingTest.kt | 526 +++++++ .../StateMachineKillFlowErrorHandlingTest.kt | 196 +++ ...> StateMachineSubFlowErrorHandlingTest.kt} | 239 +-- .../StatemachineGeneralErrorHandlingTest.kt | 1281 ----------------- .../StatemachineKillFlowErrorHandlingTest.kt | 322 ----- .../services/statemachine/ActionExecutor.kt | 2 + .../statemachine/ActionExecutorImpl.kt | 3 + .../corda/node/services/statemachine/Event.kt | 2 +- .../statemachine/FlowStateMachineImpl.kt | 13 +- .../SingleThreadedStateMachineManager.kt | 107 +- .../statemachine/StaffedFlowHospital.kt | 130 +- .../statemachine/StateTransitionExceptions.kt | 2 + .../statemachine/TransitionExecutorImpl.kt | 83 +- .../interceptors/HospitalisingInterceptor.kt | 23 +- .../transitions/DoRemainingWorkTransition.kt | 4 +- .../transitions/TopLevelTransition.kt | 4 +- .../transitions/TransitionBuilder.kt | 18 +- 21 files changed, 1636 insertions(+), 2090 deletions(-) rename node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/{StatemachineErrorHandlingTest.kt => StateMachineErrorHandlingTest.kt} (50%) rename node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/{StatemachineFinalityErrorHandlingTest.kt => StateMachineFinalityErrorHandlingTest.kt} (56%) create mode 100644 node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineFlowInitErrorHandlingTest.kt create mode 100644 node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineGeneralErrorHandlingTest.kt create mode 100644 node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineKillFlowErrorHandlingTest.kt rename node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/{StatemachineSubflowErrorHandlingTest.kt => StateMachineSubFlowErrorHandlingTest.kt} (60%) delete mode 100644 node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineGeneralErrorHandlingTest.kt delete mode 100644 node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineKillFlowErrorHandlingTest.kt diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/DatabaseTransaction.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/DatabaseTransaction.kt index d6d1425e33..3fb6b682ff 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/DatabaseTransaction.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/DatabaseTransaction.kt @@ -6,6 +6,7 @@ import org.hibernate.Session import org.hibernate.Transaction import rx.subjects.PublishSubject import java.sql.Connection +import java.sql.SQLException import java.util.UUID import javax.persistence.EntityManager @@ -79,6 +80,7 @@ class DatabaseTransaction( committed = true } + @Throws(SQLException::class) fun rollback() { if (sessionDelegate.isInitialized() && session.isOpen) { session.clear() @@ -89,16 +91,20 @@ class DatabaseTransaction( clearException() } + @Throws(SQLException::class) fun close() { - if (sessionDelegate.isInitialized() && session.isOpen) { - session.close() + try { + if (sessionDelegate.isInitialized() && session.isOpen) { + session.close() + } + if (database.closeConnection) { + connection.close() + } + } finally { + clearException() + contextTransactionOrNull = outerTransaction } - if (database.closeConnection) { - connection.close() - } - clearException() - contextTransactionOrNull = outerTransaction if (outerTransaction == null) { synchronized(this) { closed = true diff --git a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineErrorHandlingTest.kt b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineErrorHandlingTest.kt similarity index 50% rename from node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineErrorHandlingTest.kt rename to node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineErrorHandlingTest.kt index d933625407..34f44aeb86 100644 --- a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineErrorHandlingTest.kt +++ b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineErrorHandlingTest.kt @@ -3,6 +3,7 @@ package net.corda.node.services.statemachine import co.paralleluniverse.fibers.Suspendable import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowSession +import net.corda.core.flows.HospitalizeFlowException import net.corda.core.flows.InitiatedBy import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.StartableByRPC @@ -10,11 +11,14 @@ import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.internal.list import net.corda.core.internal.readAllLines +import net.corda.core.messaging.CordaRPCOps +import net.corda.core.messaging.startFlow import net.corda.core.node.AppServiceHub import net.corda.core.node.services.CordaService import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.utilities.getOrThrow +import net.corda.core.utilities.seconds import net.corda.core.utilities.unwrap import net.corda.node.services.Permissions import net.corda.testing.core.DUMMY_NOTARY_NAME @@ -23,6 +27,7 @@ import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.NodeHandle import net.corda.testing.driver.NodeParameters import net.corda.testing.driver.driver +import net.corda.testing.driver.internal.OutOfProcessImpl import net.corda.testing.node.NotarySpec import net.corda.testing.node.TestCordapp import net.corda.testing.node.User @@ -30,8 +35,11 @@ import net.corda.testing.node.internal.InternalDriverDSL import org.jboss.byteman.agent.submit.ScriptText import org.jboss.byteman.agent.submit.Submit import org.junit.Before +import java.time.Duration +import java.util.concurrent.TimeUnit +import kotlin.test.assertEquals -abstract class StatemachineErrorHandlingTest { +abstract class StateMachineErrorHandlingTest { val rpcUser = User("user1", "test", permissions = setOf(Permissions.all())) var counter = 0 @@ -57,15 +65,17 @@ abstract class StatemachineErrorHandlingTest { internal fun DriverDSL.createBytemanNode( providedName: CordaX500Name, additionalCordapps: Collection = emptyList() - ): NodeHandle { - return (this as InternalDriverDSL).startNode( + ): Pair { + val port = nextPort() + val nodeHandle = (this as InternalDriverDSL).startNode( NodeParameters( providedName = providedName, rpcUsers = listOf(rpcUser), additionalCordapps = additionalCordapps ), - bytemanPort = 12000 + bytemanPort = port ).getOrThrow() + return nodeHandle to port } internal fun DriverDSL.createNode(providedName: CordaX500Name, additionalCordapps: Collection = emptyList()): NodeHandle { @@ -78,8 +88,8 @@ abstract class StatemachineErrorHandlingTest { ).getOrThrow() } - internal fun submitBytemanRules(rules: String) { - val submit = Submit("localhost", 12000) + internal fun submitBytemanRules(rules: String, port: Int) { + val submit = Submit("localhost", port) submit.addScripts(listOf(ScriptText("Test script", rules))) } @@ -90,6 +100,37 @@ abstract class StatemachineErrorHandlingTest { .readAllLines() } + internal fun OutOfProcessImpl.stop(timeout: Duration): Boolean { + return process.run { + destroy() + waitFor(timeout.seconds, TimeUnit.SECONDS) + }.also { onStopCallback() } + } + + @Suppress("LongParameterList") + internal fun CordaRPCOps.assertHospitalCounts( + discharged: Int = 0, + observation: Int = 0, + propagated: Int = 0, + dischargedRetry: Int = 0, + observationRetry: Int = 0, + propagatedRetry: Int = 0 + ) { + val counts = startFlow(StateMachineErrorHandlingTest::GetHospitalCountersFlow).returnValue.getOrThrow(20.seconds) + assertEquals(discharged, counts.discharged) + assertEquals(observation, counts.observation) + assertEquals(propagated, counts.propagated) + assertEquals(dischargedRetry, counts.dischargeRetry) + assertEquals(observationRetry, counts.observationRetry) + assertEquals(propagatedRetry, counts.propagatedRetry) + } + + internal fun CordaRPCOps.assertHospitalCountsAllZero() = assertHospitalCounts() + + internal fun CordaRPCOps.assertNumberOfCheckpoints(number: Long) { + assertEquals(number, startFlow(StateMachineErrorHandlingTest::GetNumberOfCheckpointsFlow).returnValue.get()) + } + @StartableByRPC @InitiatingFlow class SendAMessageFlow(private val party: Party) : FlowLogic() { @@ -97,6 +138,7 @@ abstract class StatemachineErrorHandlingTest { override fun call(): String { val session = initiateFlow(party) session.send("hello there") + logger.info("Finished my flow") return "Finished executing test flow - ${this.runId}" } } @@ -106,18 +148,49 @@ abstract class StatemachineErrorHandlingTest { @Suspendable override fun call() { session.receive().unwrap { it } + logger.info("Finished my flow") + } + } + + @StartableByRPC + class ThrowAnErrorFlow : FlowLogic() { + @Suspendable + override fun call(): String { + throwException() + return "cant get here" + } + + private fun throwException() { + logger.info("Throwing exception in flow") + throw IllegalStateException("throwing exception in flow") + } + } + + @StartableByRPC + class ThrowAHospitalizeErrorFlow : FlowLogic() { + @Suspendable + override fun call(): String { + throwException() + return "cant get here" + } + + private fun throwException() { + logger.info("Throwing exception in flow") + throw HospitalizeFlowException("throwing exception in flow") } } @StartableByRPC class GetNumberOfCheckpointsFlow : FlowLogic() { override fun call(): Long { - return serviceHub.jdbcSession().prepareStatement("select count(*) from node_checkpoints").use { ps -> - ps.executeQuery().use { rs -> - rs.next() - rs.getLong(1) + return serviceHub.jdbcSession().prepareStatement("select count(*) from node_checkpoints where checkpoint_id != ?") + .apply { setString(1, runId.uuid.toString()) } + .use { ps -> + ps.executeQuery().use { rs -> + rs.next() + rs.getLong(1) + } } - } } } @@ -126,26 +199,51 @@ abstract class StatemachineErrorHandlingTest { class GetHospitalCountersFlow : FlowLogic() { override fun call(): HospitalCounts = HospitalCounts( - serviceHub.cordaService(HospitalCounter::class.java).dischargeCounter, - serviceHub.cordaService(HospitalCounter::class.java).observationCounter + serviceHub.cordaService(HospitalCounter::class.java).dischargedCounter, + serviceHub.cordaService(HospitalCounter::class.java).observationCounter, + serviceHub.cordaService(HospitalCounter::class.java).propagatedCounter, + serviceHub.cordaService(HospitalCounter::class.java).dischargeRetryCounter, + serviceHub.cordaService(HospitalCounter::class.java).observationRetryCounter, + serviceHub.cordaService(HospitalCounter::class.java).propagatedRetryCounter ) } @CordaSerializable - data class HospitalCounts(val discharge: Int, val observation: Int) + data class HospitalCounts( + val discharged: Int, + val observation: Int, + val propagated: Int, + val dischargeRetry: Int, + val observationRetry: Int, + val propagatedRetry: Int + ) @Suppress("UNUSED_PARAMETER") @CordaService class HospitalCounter(services: AppServiceHub) : SingletonSerializeAsToken() { + var dischargedCounter: Int = 0 var observationCounter: Int = 0 - var dischargeCounter: Int = 0 + var propagatedCounter: Int = 0 + var dischargeRetryCounter: Int = 0 + var observationRetryCounter: Int = 0 + var propagatedRetryCounter: Int = 0 init { StaffedFlowHospital.onFlowDischarged.add { _, _ -> - ++dischargeCounter + dischargedCounter++ } StaffedFlowHospital.onFlowKeptForOvernightObservation.add { _, _ -> - ++observationCounter + observationCounter++ + } + StaffedFlowHospital.onFlowErrorPropagated.add { _, _ -> + propagatedCounter++ + } + StaffedFlowHospital.onFlowResuscitated.add { _, _, outcome -> + when (outcome) { + StaffedFlowHospital.Outcome.DISCHARGE -> dischargeRetryCounter++ + StaffedFlowHospital.Outcome.OVERNIGHT_OBSERVATION -> observationRetryCounter++ + StaffedFlowHospital.Outcome.UNTREATABLE -> propagatedRetryCounter++ + } } } } diff --git a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineFinalityErrorHandlingTest.kt b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineFinalityErrorHandlingTest.kt similarity index 56% rename from node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineFinalityErrorHandlingTest.kt rename to node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineFinalityErrorHandlingTest.kt index 1855aa11c3..898012a3d8 100644 --- a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineFinalityErrorHandlingTest.kt +++ b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineFinalityErrorHandlingTest.kt @@ -1,6 +1,5 @@ package net.corda.node.services.statemachine -import net.corda.client.rpc.CordaRPCClient import net.corda.core.flows.ReceiveFinalityFlow import net.corda.core.internal.ResolveTransactionsFlow import net.corda.core.messaging.startFlow @@ -22,7 +21,7 @@ import kotlin.test.assertEquals import kotlin.test.assertFailsWith @Suppress("MaxLineLength") // Byteman rules cannot be easily wrapped -class StatemachineFinalityErrorHandlingTest : StatemachineErrorHandlingTest() { +class StateMachineFinalityErrorHandlingTest : StateMachineErrorHandlingTest() { /** * Throws an exception when recoding a transaction inside of [ReceiveFinalityFlow] on the responding @@ -33,10 +32,10 @@ class StatemachineFinalityErrorHandlingTest : StatemachineErrorHandlingTest() { * Only the responding node keeps a checkpoint. The initiating flow has completed successfully as it has complete its * send to the responding node and the responding node successfully received it. */ - @Test(timeout=300_000) - fun `error recording a transaction inside of ReceiveFinalityFlow will keep the flow in for observation`() { + @Test(timeout = 300_000) + fun `error recording a transaction inside of ReceiveFinalityFlow will keep the flow in for observation`() { startDriver(notarySpec = NotarySpec(DUMMY_NOTARY_NAME, validating = false)) { - val charlie = createBytemanNode(CHARLIE_NAME, FINANCE_CORDAPPS) + val (charlie, port) = createBytemanNode(CHARLIE_NAME, FINANCE_CORDAPPS) val alice = createNode(ALICE_NAME, FINANCE_CORDAPPS) // could not get rule for FinalityDoctor + observation counter to work @@ -67,14 +66,9 @@ class StatemachineFinalityErrorHandlingTest : StatemachineErrorHandlingTest() { ENDRULE """.trimIndent() - submitBytemanRules(rules) + submitBytemanRules(rules, port) - val aliceClient = - CordaRPCClient(alice.rpcAddress).start(rpcUser.username, rpcUser.password).proxy - val charlieClient = - CordaRPCClient(charlie.rpcAddress).start(rpcUser.username, rpcUser.password).proxy - - aliceClient.startFlow( + alice.rpc.startFlow( ::CashIssueAndPaymentFlow, 500.DOLLARS, OpaqueBytes.of(0x01), @@ -83,15 +77,11 @@ class StatemachineFinalityErrorHandlingTest : StatemachineErrorHandlingTest() { defaultNotaryIdentity ).returnValue.getOrThrow(30.seconds) - val (discharge, observation) = charlieClient.startFlow(StatemachineErrorHandlingTest::GetHospitalCountersFlow).returnValue.get() - assertEquals(0, discharge) - assertEquals(1, observation) - assertEquals(0, aliceClient.stateMachinesSnapshot().size) - assertEquals(1, charlieClient.stateMachinesSnapshot().size) - // 1 for GetNumberOfCheckpointsFlow - assertEquals(1, aliceClient.startFlow(StatemachineErrorHandlingTest::GetNumberOfCheckpointsFlow).returnValue.get()) - // 1 ReceiveFinalityFlow and 1 for GetNumberOfCheckpointsFlow - assertEquals(2, charlieClient.startFlow(StatemachineErrorHandlingTest::GetNumberOfCheckpointsFlow).returnValue.get()) + charlie.rpc.assertHospitalCounts(observation = 1) + assertEquals(0, alice.rpc.stateMachinesSnapshot().size) + assertEquals(1, charlie.rpc.stateMachinesSnapshot().size) + alice.rpc.assertNumberOfCheckpoints(0) + charlie.rpc.assertNumberOfCheckpoints(1) } } @@ -104,10 +94,10 @@ class StatemachineFinalityErrorHandlingTest : StatemachineErrorHandlingTest() { * Only the responding node keeps a checkpoint. The initiating flow has completed successfully as it has complete its * send to the responding node and the responding node successfully received it. */ - @Test(timeout=300_000) - fun `error resolving a transaction's dependencies inside of ReceiveFinalityFlow will keep the flow in for observation`() { + @Test(timeout = 300_000) + fun `error resolving a transaction's dependencies inside of ReceiveFinalityFlow will keep the flow in for observation`() { startDriver(notarySpec = NotarySpec(DUMMY_NOTARY_NAME, validating = false)) { - val charlie = createBytemanNode(CHARLIE_NAME, FINANCE_CORDAPPS) + val (charlie, port) = createBytemanNode(CHARLIE_NAME, FINANCE_CORDAPPS) val alice = createNode(ALICE_NAME, FINANCE_CORDAPPS) // could not get rule for FinalityDoctor + observation counter to work @@ -138,14 +128,9 @@ class StatemachineFinalityErrorHandlingTest : StatemachineErrorHandlingTest() { ENDRULE """.trimIndent() - submitBytemanRules(rules) + submitBytemanRules(rules, port) - val aliceClient = - CordaRPCClient(alice.rpcAddress).start(rpcUser.username, rpcUser.password).proxy - val charlieClient = - CordaRPCClient(charlie.rpcAddress).start(rpcUser.username, rpcUser.password).proxy - - aliceClient.startFlow( + alice.rpc.startFlow( ::CashIssueAndPaymentFlow, 500.DOLLARS, OpaqueBytes.of(0x01), @@ -154,15 +139,11 @@ class StatemachineFinalityErrorHandlingTest : StatemachineErrorHandlingTest() { defaultNotaryIdentity ).returnValue.getOrThrow(30.seconds) - val (discharge, observation) = charlieClient.startFlow(StatemachineErrorHandlingTest::GetHospitalCountersFlow).returnValue.get() - assertEquals(0, discharge) - assertEquals(1, observation) - assertEquals(0, aliceClient.stateMachinesSnapshot().size) - assertEquals(1, charlieClient.stateMachinesSnapshot().size) - // 1 for GetNumberOfCheckpointsFlow - assertEquals(1, aliceClient.startFlow(StatemachineErrorHandlingTest::GetNumberOfCheckpointsFlow).returnValue.get()) - // 1 for ReceiveFinalityFlow and 1 for GetNumberOfCheckpointsFlow - assertEquals(2, charlieClient.startFlow(StatemachineErrorHandlingTest::GetNumberOfCheckpointsFlow).returnValue.get()) + charlie.rpc.assertHospitalCounts(observation = 1) + assertEquals(0, alice.rpc.stateMachinesSnapshot().size) + assertEquals(1, charlie.rpc.stateMachinesSnapshot().size) + alice.rpc.assertNumberOfCheckpoints(0) + charlie.rpc.assertNumberOfCheckpoints(1) } } @@ -170,17 +151,17 @@ class StatemachineFinalityErrorHandlingTest : StatemachineErrorHandlingTest() { * Throws an exception when executing [Action.CommitTransaction] as part of receiving a transaction to record inside of [ReceiveFinalityFlow] on the responding * flow's node. * - * The exception is thrown 5 times. + * The exception is thrown 3 times. * * The responding flow is retried 3 times and then completes successfully. * * The [StaffedFlowHospital.TransitionErrorGeneralPractitioner] catches these errors instead of the [StaffedFlowHospital.FinalityDoctor]. Due to this, the * flow is retried instead of moving straight to observation. */ - @Test(timeout=300_000) - fun `error during transition with CommitTransaction action while receiving a transaction inside of ReceiveFinalityFlow will be retried and complete successfully`() { + @Test(timeout = 300_000) + fun `error during transition with CommitTransaction action while receiving a transaction inside of ReceiveFinalityFlow will be retried and complete successfully`() { startDriver(notarySpec = NotarySpec(DUMMY_NOTARY_NAME, validating = false)) { - val charlie = createBytemanNode(CHARLIE_NAME, FINANCE_CORDAPPS) + val (charlie, port) = createBytemanNode(CHARLIE_NAME, FINANCE_CORDAPPS) val alice = createNode(ALICE_NAME, FINANCE_CORDAPPS) val rules = """ @@ -204,35 +185,14 @@ class StatemachineFinalityErrorHandlingTest : StatemachineErrorHandlingTest() { CLASS ${ActionExecutorImpl::class.java.name} METHOD executeCommitTransaction AT ENTRY - IF flagged("finality_flag") && readCounter("counter") < 5 + IF flagged("finality_flag") && readCounter("counter") < 3 DO incrementCounter("counter"); traceln("Throwing exception"); throw new java.lang.RuntimeException("die dammit die") ENDRULE - - RULE Increment discharge counter - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT READ DISCHARGE - IF true - DO traceln("Byteman test - discharging") - ENDRULE - - RULE Increment observation counter - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT READ OVERNIGHT_OBSERVATION - IF true - DO traceln("Byteman test - overnight observation") - ENDRULE """.trimIndent() - submitBytemanRules(rules) + submitBytemanRules(rules, port) - val aliceClient = - CordaRPCClient(alice.rpcAddress).start(rpcUser.username, rpcUser.password).proxy - val charlieClient = - CordaRPCClient(charlie.rpcAddress).start(rpcUser.username, rpcUser.password).proxy - - aliceClient.startFlow( + alice.rpc.startFlow( ::CashIssueAndPaymentFlow, 500.DOLLARS, OpaqueBytes.of(0x01), @@ -241,20 +201,11 @@ class StatemachineFinalityErrorHandlingTest : StatemachineErrorHandlingTest() { defaultNotaryIdentity ).returnValue.getOrThrow(30.seconds) - val output = getBytemanOutput(charlie) - - // Check the stdout for the lines generated by byteman - assertEquals(3, output.filter { it.contains("Byteman test - discharging") }.size) - assertEquals(0, output.filter { it.contains("Byteman test - overnight observation") }.size) - val (discharge, observation) = charlieClient.startFlow(StatemachineErrorHandlingTest::GetHospitalCountersFlow).returnValue.get() - assertEquals(3, discharge) - assertEquals(0, observation) - assertEquals(0, aliceClient.stateMachinesSnapshot().size) - assertEquals(0, charlieClient.stateMachinesSnapshot().size) - // 1 for GetNumberOfCheckpointsFlow - assertEquals(1, aliceClient.startFlow(StatemachineErrorHandlingTest::GetNumberOfCheckpointsFlow).returnValue.get()) - // 1 for GetNumberOfCheckpointsFlow - assertEquals(1, charlieClient.startFlow(StatemachineErrorHandlingTest::GetNumberOfCheckpointsFlow).returnValue.get()) + charlie.rpc.assertHospitalCounts(discharged = 3) + assertEquals(0, alice.rpc.stateMachinesSnapshot().size) + assertEquals(0, charlie.rpc.stateMachinesSnapshot().size) + alice.rpc.assertNumberOfCheckpoints(0) + charlie.rpc.assertNumberOfCheckpoints(0) } } @@ -262,7 +213,7 @@ class StatemachineFinalityErrorHandlingTest : StatemachineErrorHandlingTest() { * Throws an exception when executing [Action.CommitTransaction] as part of receiving a transaction to record inside of [ReceiveFinalityFlow] on the responding * flow's node. * - * The exception is thrown 7 times. + * The exception is thrown 4 times. * * The responding flow is retried 3 times and is then kept in for observation. * @@ -272,10 +223,10 @@ class StatemachineFinalityErrorHandlingTest : StatemachineErrorHandlingTest() { * The [StaffedFlowHospital.TransitionErrorGeneralPractitioner] catches these errors instead of the [StaffedFlowHospital.FinalityDoctor]. Due to this, the * flow is retried instead of moving straight to observation. */ - @Test(timeout=300_000) - fun `error during transition with CommitTransaction action while receiving a transaction inside of ReceiveFinalityFlow will be retried and be kept for observation is error persists`() { + @Test(timeout = 300_000) + fun `error during transition with CommitTransaction action while receiving a transaction inside of ReceiveFinalityFlow will be retried and be kept for observation is error persists`() { startDriver(notarySpec = NotarySpec(DUMMY_NOTARY_NAME, validating = false)) { - val charlie = createBytemanNode(CHARLIE_NAME, FINANCE_CORDAPPS) + val (charlie, port) = createBytemanNode(CHARLIE_NAME, FINANCE_CORDAPPS) val alice = createNode(ALICE_NAME, FINANCE_CORDAPPS) val rules = """ @@ -299,36 +250,15 @@ class StatemachineFinalityErrorHandlingTest : StatemachineErrorHandlingTest() { CLASS ${ActionExecutorImpl::class.java.name} METHOD executeCommitTransaction AT ENTRY - IF flagged("finality_flag") && readCounter("counter") < 7 + IF flagged("finality_flag") && readCounter("counter") < 4 DO incrementCounter("counter"); traceln("Throwing exception"); throw new java.lang.RuntimeException("die dammit die") ENDRULE - - RULE Increment discharge counter - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT READ DISCHARGE - IF true - DO traceln("Byteman test - discharging") - ENDRULE - - RULE Increment observation counter - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT READ OVERNIGHT_OBSERVATION - IF true - DO traceln("Byteman test - overnight observation") - ENDRULE """.trimIndent() - submitBytemanRules(rules) - - val aliceClient = - CordaRPCClient(alice.rpcAddress).start(rpcUser.username, rpcUser.password).proxy - val charlieClient = - CordaRPCClient(charlie.rpcAddress).start(rpcUser.username, rpcUser.password).proxy + submitBytemanRules(rules, port) assertFailsWith { - aliceClient.startFlow( + alice.rpc.startFlow( ::CashIssueAndPaymentFlow, 500.DOLLARS, OpaqueBytes.of(0x01), @@ -338,20 +268,14 @@ class StatemachineFinalityErrorHandlingTest : StatemachineErrorHandlingTest() { ).returnValue.getOrThrow(30.seconds) } - val output = getBytemanOutput(charlie) - - // Check the stdout for the lines generated by byteman - assertEquals(3, output.filter { it.contains("Byteman test - discharging") }.size) - assertEquals(1, output.filter { it.contains("Byteman test - overnight observation") }.size) - val (discharge, observation) = charlieClient.startFlow(StatemachineErrorHandlingTest::GetHospitalCountersFlow).returnValue.get() - assertEquals(3, discharge) - assertEquals(1, observation) - assertEquals(1, aliceClient.stateMachinesSnapshot().size) - assertEquals(1, charlieClient.stateMachinesSnapshot().size) - // 1 for CashIssueAndPaymentFlow and 1 for GetNumberOfCheckpointsFlow - assertEquals(2, aliceClient.startFlow(StatemachineErrorHandlingTest::GetNumberOfCheckpointsFlow).returnValue.get()) - // 1 for ReceiveFinalityFlow and 1 for GetNumberOfCheckpointsFlow - assertEquals(2, charlieClient.startFlow(StatemachineErrorHandlingTest::GetNumberOfCheckpointsFlow).returnValue.get()) + charlie.rpc.assertHospitalCounts( + discharged = 3, + observation = 1 + ) + assertEquals(1, alice.rpc.stateMachinesSnapshot().size) + assertEquals(1, charlie.rpc.stateMachinesSnapshot().size) + alice.rpc.assertNumberOfCheckpoints(1) + charlie.rpc.assertNumberOfCheckpoints(1) } } } \ No newline at end of file diff --git a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineFlowInitErrorHandlingTest.kt b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineFlowInitErrorHandlingTest.kt new file mode 100644 index 0000000000..8e925ec6a1 --- /dev/null +++ b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineFlowInitErrorHandlingTest.kt @@ -0,0 +1,447 @@ +package net.corda.node.services.statemachine + +import net.corda.core.CordaRuntimeException +import net.corda.core.messaging.startFlow +import net.corda.core.utilities.getOrThrow +import net.corda.core.utilities.seconds +import net.corda.testing.core.ALICE_NAME +import net.corda.testing.core.CHARLIE_NAME +import net.corda.testing.core.singleIdentity +import net.corda.testing.driver.internal.OutOfProcessImpl +import org.junit.Test +import java.sql.Connection +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue + +@Suppress("MaxLineLength") // Byteman rules cannot be easily wrapped +class StateMachineFlowInitErrorHandlingTest : StateMachineErrorHandlingTest() { + + private companion object { + val executor: ExecutorService = Executors.newSingleThreadExecutor() + } + + /** + * Throws an exception when performing an [Action.CommitTransaction] event before the flow has initialised and saved its first checkpoint + * (remains in an unstarted state). + * + * The exception is thrown 3 times. + * + * This causes the transition to be discharged from the hospital 3 times (retries 3 times). On the final retry the transition + * succeeds and the flow finishes. + * + * Each time the flow retries, it starts from the beginning of the flow (due to being in an unstarted state). + * + */ + @Test(timeout = 300_000) + fun `error during transition with CommitTransaction action that occurs during flow initialisation will retry and complete successfully`() { + startDriver { + val charlie = createNode(CHARLIE_NAME) + val (alice, port) = createBytemanNode(ALICE_NAME) + + val rules = """ + RULE Create Counter + CLASS ${ActionExecutorImpl::class.java.name} + METHOD executeCommitTransaction + AT ENTRY + IF createCounter("counter", $counter) + DO traceln("Counter created") + ENDRULE + + RULE Throw exception on executeCommitTransaction action + CLASS ${ActionExecutorImpl::class.java.name} + METHOD executeCommitTransaction + AT ENTRY + IF readCounter("counter") < 3 + DO incrementCounter("counter"); traceln("Throwing exception"); throw new java.sql.SQLException("die dammit die", "1") + ENDRULE + """.trimIndent() + + submitBytemanRules(rules, port) + + alice.rpc.startFlow( + StateMachineErrorHandlingTest::SendAMessageFlow, + charlie.nodeInfo.singleIdentity() + ).returnValue.getOrThrow( + 30.seconds + ) + + alice.rpc.assertHospitalCounts(discharged = 3) + assertEquals(0, alice.rpc.stateMachinesSnapshot().size) + alice.rpc.assertNumberOfCheckpoints(0) + } + } + + /** + * Throws an exception when calling [FlowStateMachineImpl.processEvent]. + * + * This is not an expected place for an exception to occur, but allows us to test what happens when a random exception is propagated + * up to [FlowStateMachineImpl.run] during flow initialisation. + * + * A "Transaction context is missing" exception is thrown due to where the exception is thrown (no transaction is created so this is + * thrown when leaving [FlowStateMachineImpl.processEventsUntilFlowIsResumed] due to the finally block). + */ + @Test(timeout = 300_000) + fun `unexpected error during flow initialisation throws exception to client`() { + startDriver { + val charlie = createNode(CHARLIE_NAME) + val (alice, port) = createBytemanNode(ALICE_NAME) + val rules = """ + RULE Create Counter + CLASS ${FlowStateMachineImpl::class.java.name} + METHOD processEvent + AT ENTRY + IF createCounter("counter", $counter) + DO traceln("Counter created") + ENDRULE + + RULE Throw exception + CLASS ${FlowStateMachineImpl::class.java.name} + METHOD processEvent + AT ENTRY + IF readCounter("counter") < 1 + DO incrementCounter("counter"); traceln("Throwing exception"); throw new java.lang.RuntimeException("die dammit die") + ENDRULE + """.trimIndent() + + submitBytemanRules(rules, port) + + assertFailsWith { + alice.rpc.startFlow( + StateMachineErrorHandlingTest::SendAMessageFlow, + charlie.nodeInfo.singleIdentity() + ).returnValue.getOrThrow(30.seconds) + } + + alice.rpc.assertHospitalCounts(propagated = 1) + assertEquals(0, alice.rpc.stateMachinesSnapshot().size) + alice.rpc.assertNumberOfCheckpoints(0) + } + } + + /** + * Throws an exception when performing an [Action.CommitTransaction] event before the flow has initialised and saved its first checkpoint + * (remains in an unstarted state). + * + * A [SQLException] is then thrown when trying to rollback the flow's database transaction. + * + * The [SQLException] should be suppressed and the flow should continue to retry and complete successfully. + */ + @Test(timeout = 300_000) + fun `error during initialisation when trying to rollback the flow's database transaction the flow is able to retry and complete successfully`() { + startDriver { + val charlie = createNode(CHARLIE_NAME) + val (alice, port) = createBytemanNode(ALICE_NAME) + + val rules = """ + RULE Create Counter + CLASS ${ActionExecutorImpl::class.java.name} + METHOD executeCommitTransaction + AT ENTRY + IF createCounter("counter", $counter) + DO traceln("Counter created") + ENDRULE + + RULE Throw exception on executeCommitTransaction action + CLASS ${ActionExecutorImpl::class.java.name} + METHOD executeCommitTransaction + AT ENTRY + IF readCounter("counter") == 0 + DO incrementCounter("counter"); traceln("Throwing exception"); throw new java.lang.RuntimeException("die dammit die") + ENDRULE + + RULE Throw exception when rolling back transaction in transition executor + INTERFACE ${Connection::class.java.name} + METHOD rollback + AT ENTRY + IF readCounter("counter") == 1 + DO incrementCounter("counter"); traceln("Throwing exception in transition executor"); throw new java.sql.SQLException("could not reach db", "1") + ENDRULE + """.trimIndent() + + submitBytemanRules(rules, port) + + alice.rpc.startFlow( + StateMachineErrorHandlingTest::SendAMessageFlow, + charlie.nodeInfo.singleIdentity() + ).returnValue.getOrThrow(30.seconds) + + alice.rpc.assertHospitalCounts(discharged = 1) + assertEquals(0, alice.rpc.stateMachinesSnapshot().size) + alice.rpc.assertNumberOfCheckpoints(0) + } + } + + /** + * Throws an exception when performing an [Action.CommitTransaction] event before the flow has initialised and saved its first checkpoint + * (remains in an unstarted state). + * + * A [SQLException] is then thrown when trying to close the flow's database transaction. + * + * The [SQLException] should be suppressed and the flow should continue to retry and complete successfully. + */ + @Test(timeout = 300_000) + fun `error during initialisation when trying to close the flow's database transaction the flow is able to retry and complete successfully`() { + startDriver { + val charlie = createNode(CHARLIE_NAME) + val (alice, port) = createBytemanNode(ALICE_NAME) + + val rules = """ + RULE Create Counter + CLASS ${ActionExecutorImpl::class.java.name} + METHOD executeCommitTransaction + AT ENTRY + IF createCounter("counter", $counter) + DO traceln("Counter created") + ENDRULE + + RULE Throw exception on executeCommitTransaction action + CLASS ${ActionExecutorImpl::class.java.name} + METHOD executeCommitTransaction + AT ENTRY + IF readCounter("counter") == 0 + DO incrementCounter("counter"); traceln("Throwing exception"); throw new java.lang.RuntimeException("die dammit die") + ENDRULE + + RULE Throw exception when rolling back transaction in transition executor + INTERFACE ${Connection::class.java.name} + METHOD close + AT ENTRY + IF readCounter("counter") == 1 + DO incrementCounter("counter"); traceln("Throwing exception in transition executor"); throw new java.sql.SQLException("could not reach db", "1") + ENDRULE + """.trimIndent() + + submitBytemanRules(rules, port) + + alice.rpc.startFlow( + StateMachineErrorHandlingTest::SendAMessageFlow, + charlie.nodeInfo.singleIdentity() + ).returnValue.getOrThrow(30.seconds) + + alice.rpc.assertHospitalCounts(discharged = 1) + assertEquals(0, alice.rpc.stateMachinesSnapshot().size) + alice.rpc.assertNumberOfCheckpoints(0) + } + } + + /** + * Throws an exception when performing an [Action.CommitTransaction] event before the flow has initialised and saved its first checkpoint + * (remains in an unstarted state). + * + * The exception is thrown 4 times. + * + * This causes the transition to be discharged from the hospital 3 times (retries 3 times) and then be kept in for observation. + * + * Each time the flow retries, it starts from the beginning of the flow (due to being in an unstarted state). + */ + @Test(timeout = 300_000) + fun `error during transition with CommitTransaction action that occurs during flow initialisation will retry and be kept for observation if error persists`() { + startDriver { + val charlie = createNode(CHARLIE_NAME) + val (alice, port) = createBytemanNode(ALICE_NAME) + + val rules = """ + RULE Create Counter + CLASS ${ActionExecutorImpl::class.java.name} + METHOD executeCommitTransaction + AT ENTRY + IF createCounter("counter", $counter) + DO traceln("Counter created") + ENDRULE + + RULE Throw exception on executeCommitTransaction action + CLASS ${ActionExecutorImpl::class.java.name} + METHOD executeCommitTransaction + AT ENTRY + IF readCounter("counter") < 4 + DO incrementCounter("counter"); traceln("Throwing exception"); throw new java.sql.SQLException("die dammit die", "1") + ENDRULE + """.trimIndent() + + submitBytemanRules(rules, port) + + executor.execute { + alice.rpc.startFlow(StateMachineErrorHandlingTest::SendAMessageFlow, charlie.nodeInfo.singleIdentity()) + } + + // flow is not signaled as started calls to [getOrThrow] will hang, sleeping instead + Thread.sleep(30.seconds.toMillis()) + + alice.rpc.assertHospitalCounts( + discharged = 3, + observation = 1 + ) + assertEquals(1, alice.rpc.stateMachinesSnapshot().size) + alice.rpc.assertNumberOfCheckpoints(0) + val terminated = (alice as OutOfProcessImpl).stop(60.seconds) + assertTrue(terminated, "The node must be shutdown before it can be restarted") + val (alice2, _) = createBytemanNode(ALICE_NAME) + Thread.sleep(10.seconds.toMillis()) + alice2.rpc.assertNumberOfCheckpoints(0) + } + } + + /** + * Throws an exception when performing an [Action.CommitTransaction] event before the flow has initialised and saved its first checkpoint + * (remains in an unstarted state). + * + * An exception is thrown when committing a database transaction during a transition to trigger the retry of the flow. Another + * exception is then thrown during the retry itself. + * + * The flow then retries the retry causing the flow to complete successfully. + */ + @Test(timeout = 300_000) + fun `error during retrying a flow that failed when committing its original checkpoint will retry the flow again and complete successfully`() { + startDriver { + val charlie = createNode(CHARLIE_NAME) + val (alice, port) = createBytemanNode(ALICE_NAME) + + val rules = """ + RULE Throw exception on executeCommitTransaction action after first suspend + commit + CLASS ${ActionExecutorImpl::class.java.name} + METHOD executeCommitTransaction + AT ENTRY + IF !flagged("commit_exception_flag") + DO flag("commit_exception_flag"); traceln("Throwing exception"); throw new java.sql.SQLException("die dammit die", "1") + ENDRULE + + RULE Throw exception on retry + CLASS ${SingleThreadedStateMachineManager::class.java.name} + METHOD onExternalStartFlow + AT ENTRY + IF flagged("commit_exception_flag") && !flagged("retry_exception_flag") + DO flag("retry_exception_flag"); traceln("Throwing retry exception"); throw new java.lang.RuntimeException("Here we go again") + ENDRULE + """.trimIndent() + + submitBytemanRules(rules, port) + + alice.rpc.startFlow( + StateMachineErrorHandlingTest::SendAMessageFlow, + charlie.nodeInfo.singleIdentity() + ).returnValue.getOrThrow( + 30.seconds + ) + + alice.rpc.assertHospitalCounts( + discharged = 1, + dischargedRetry = 1 + ) + assertEquals(0, alice.rpc.stateMachinesSnapshot().size) + alice.rpc.assertNumberOfCheckpoints(0) + } + } + + /** + * Throws an exception when performing an [Action.CommitTransaction] event on a responding node before the flow has initialised and + * saved its first checkpoint (remains in an unstarted state). + * + * The exception is thrown 3 times. + * + * This causes the transition to be discharged from the hospital 3 times (retries 3 times). On the final retry the transition + * succeeds and the flow finishes. + * + * Each time the flow retries, it starts from the beginning of the flow (due to being in an unstarted state). + */ + @Test(timeout = 300_000) + fun `responding flow - error during transition with CommitTransaction action that occurs during flow initialisation will retry and complete successfully`() { + startDriver { + val (charlie, port) = createBytemanNode(CHARLIE_NAME) + val alice = createNode(ALICE_NAME) + + val rules = """ + RULE Create Counter + CLASS ${ActionExecutorImpl::class.java.name} + METHOD executeCommitTransaction + AT ENTRY + IF createCounter("counter", $counter) + DO traceln("Counter created") + ENDRULE + + RULE Throw exception on executeCommitTransaction action + CLASS ${ActionExecutorImpl::class.java.name} + METHOD executeCommitTransaction + AT ENTRY + IF readCounter("counter") < 3 + DO incrementCounter("counter"); traceln("Throwing exception"); throw new java.sql.SQLException("die dammit die", "1") + ENDRULE + """.trimIndent() + + submitBytemanRules(rules, port) + + alice.rpc.startFlow( + StateMachineErrorHandlingTest::SendAMessageFlow, + charlie.nodeInfo.singleIdentity() + ).returnValue.getOrThrow( + 30.seconds + ) + + charlie.rpc.assertHospitalCounts(discharged = 3) + alice.rpc.assertNumberOfCheckpoints(0) + charlie.rpc.assertNumberOfCheckpoints(0) + } + } + + /** + * Throws an exception when performing an [Action.CommitTransaction] event on a responding node before the flow has initialised and + * saved its first checkpoint (remains in an unstarted state). + * + * The exception is thrown 4 times. + * + * This causes the transition to be discharged from the hospital 3 times (retries 3 times) and then be kept in for observation. + * + * Each time the flow retries, it starts from the beginning of the flow (due to being in an unstarted state). + */ + @Test(timeout = 300_000) + fun `responding flow - error during transition with CommitTransaction action that occurs during flow initialisation will retry and be kept for observation if error persists`() { + startDriver { + val (charlie, port) = createBytemanNode(CHARLIE_NAME) + val alice = createNode(ALICE_NAME) + + val rules = """ + RULE Create Counter + CLASS ${ActionExecutorImpl::class.java.name} + METHOD executeCommitTransaction + AT ENTRY + IF createCounter("counter", $counter) + DO traceln("Counter created") + ENDRULE + + RULE Throw exception on executeCommitTransaction action + CLASS ${ActionExecutorImpl::class.java.name} + METHOD executeCommitTransaction + AT ENTRY + IF readCounter("counter") < 4 + DO incrementCounter("counter"); traceln("Throwing exception"); throw new java.sql.SQLException("die dammit die", "1") + ENDRULE + """.trimIndent() + + submitBytemanRules(rules, port) + + executor.execute { + alice.rpc.startFlow(StateMachineErrorHandlingTest::SendAMessageFlow, charlie.nodeInfo.singleIdentity()) + } + + // flow is not signaled as started calls to [getOrThrow] will hang, sleeping instead + Thread.sleep(30.seconds.toMillis()) + + charlie.rpc.assertHospitalCounts( + discharged = 3, + observation = 1 + ) + assertEquals(1, alice.rpc.stateMachinesSnapshot().size) + assertEquals(1, charlie.rpc.stateMachinesSnapshot().size) + alice.rpc.assertNumberOfCheckpoints(1) + charlie.rpc.assertNumberOfCheckpoints(0) + val terminated = (charlie as OutOfProcessImpl).stop(60.seconds) + assertTrue(terminated, "The node must be shutdown before it can be restarted") + val (charlie2, _) = createBytemanNode(CHARLIE_NAME) + Thread.sleep(10.seconds.toMillis()) + alice.rpc.assertNumberOfCheckpoints(0) + charlie2.rpc.assertNumberOfCheckpoints(0) + } + } +} \ No newline at end of file diff --git a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineGeneralErrorHandlingTest.kt b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineGeneralErrorHandlingTest.kt new file mode 100644 index 0000000000..62c18d4107 --- /dev/null +++ b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineGeneralErrorHandlingTest.kt @@ -0,0 +1,526 @@ +package net.corda.node.services.statemachine + +import net.corda.core.CordaRuntimeException +import net.corda.core.messaging.startFlow +import net.corda.core.utilities.getOrThrow +import net.corda.core.utilities.seconds +import net.corda.node.services.messaging.DeduplicationHandler +import net.corda.node.services.statemachine.transitions.TopLevelTransition +import net.corda.testing.core.ALICE_NAME +import net.corda.testing.core.CHARLIE_NAME +import net.corda.testing.core.singleIdentity +import org.junit.Test +import java.util.concurrent.TimeoutException +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +@Suppress("MaxLineLength") // Byteman rules cannot be easily wrapped +class StateMachineGeneralErrorHandlingTest : StateMachineErrorHandlingTest() { + + /** + * Throws an exception when performing an [Action.SendInitial] action. + * + * The exception is thrown 4 times. + * + * This causes the transition to be discharged from the hospital 3 times (retries 3 times) and is then kept in + * the hospital for observation. + */ + @Test(timeout = 300_000) + fun `error during transition with SendInitial action is retried 3 times and kept for observation if error persists`() { + startDriver { + val charlie = createNode(CHARLIE_NAME) + val (alice, port) = createBytemanNode(ALICE_NAME) + + val rules = """ + RULE Create Counter + CLASS ${ActionExecutorImpl::class.java.name} + METHOD executeSendInitial + AT ENTRY + IF createCounter("counter", $counter) + DO traceln("Counter created") + ENDRULE + + RULE Throw exception on executeSendInitial action + CLASS ${ActionExecutorImpl::class.java.name} + METHOD executeSendInitial + AT ENTRY + IF readCounter("counter") < 4 + DO incrementCounter("counter"); traceln("Throwing exception"); throw new java.lang.RuntimeException("die dammit die") + ENDRULE + """.trimIndent() + + submitBytemanRules(rules, port) + + assertFailsWith { + alice.rpc.startFlow( + StateMachineErrorHandlingTest::SendAMessageFlow, + charlie.nodeInfo.singleIdentity() + ).returnValue.getOrThrow( + 30.seconds + ) + } + + alice.rpc.assertHospitalCounts( + discharged = 3, + observation = 1 + ) + assertEquals(1, alice.rpc.stateMachinesSnapshot().size) + alice.rpc.assertNumberOfCheckpoints(1) + } + } + + /** + * Throws an exception when performing an [Action.SendInitial] event. + * + * The exception is thrown 3 times. + * + * This causes the transition to be discharged from the hospital 3 times (retries 3 times). On the final retry the transition + * succeeds and the flow finishes. + */ + @Test(timeout = 300_000) + fun `error during transition with SendInitial action that does not persist will retry and complete successfully`() { + startDriver { + val charlie = createNode(CHARLIE_NAME) + val (alice, port) = createBytemanNode(ALICE_NAME) + + val rules = """ + RULE Create Counter + CLASS ${ActionExecutorImpl::class.java.name} + METHOD executeSendInitial + AT ENTRY + IF createCounter("counter", $counter) + DO traceln("Counter created") + ENDRULE + + RULE Throw exception on executeSendInitial action + CLASS ${ActionExecutorImpl::class.java.name} + METHOD executeSendInitial + AT ENTRY + IF readCounter("counter") < 3 + DO incrementCounter("counter"); traceln("Throwing exception"); throw new java.lang.RuntimeException("die dammit die") + ENDRULE + """.trimIndent() + + submitBytemanRules(rules, port) + + alice.rpc.startFlow( + StateMachineErrorHandlingTest::SendAMessageFlow, + charlie.nodeInfo.singleIdentity() + ).returnValue.getOrThrow( + 30.seconds + ) + + alice.rpc.assertHospitalCounts(discharged = 3) + assertEquals(0, alice.rpc.stateMachinesSnapshot().size) + alice.rpc.assertNumberOfCheckpoints(0) + } + } + + /** + * Throws an exception when executing [DeduplicationHandler.afterDatabaseTransaction] from inside an [Action.AcknowledgeMessages] action. + * + * The exception is thrown every time [DeduplicationHandler.afterDatabaseTransaction] is executed inside of + * [ActionExecutorImpl.executeAcknowledgeMessages] + * + * The exceptions should be swallowed. Therefore there should be no trips to the hospital and no retries. + * The flow should complete successfully as the error is swallowed. + */ + @Test(timeout = 300_000) + fun `error during transition with AcknowledgeMessages action is swallowed and flow completes successfully`() { + startDriver { + val charlie = createNode(CHARLIE_NAME) + val (alice, port) = createBytemanNode(ALICE_NAME) + + val rules = """ + RULE Set flag when inside executeAcknowledgeMessages + CLASS ${ActionExecutorImpl::class.java.name} + METHOD executeAcknowledgeMessages + AT INVOKE ${DeduplicationHandler::class.java.name}.afterDatabaseTransaction() + IF !flagged("exception_flag") + DO flag("exception_flag"); traceln("Setting flag to true") + ENDRULE + + RULE Throw exception when executing ${DeduplicationHandler::class.java.name}.afterDatabaseTransaction when inside executeAcknowledgeMessages + INTERFACE ${DeduplicationHandler::class.java.name} + METHOD afterDatabaseTransaction + AT ENTRY + IF flagged("exception_flag") + DO traceln("Throwing exception"); clear("exception_flag"); traceln("SETTING FLAG TO FALSE"); throw new java.lang.RuntimeException("die dammit die") + ENDRULE + """.trimIndent() + + submitBytemanRules(rules, port) + + alice.rpc.startFlow( + StateMachineErrorHandlingTest::SendAMessageFlow, + charlie.nodeInfo.singleIdentity() + ).returnValue.getOrThrow( + 30.seconds + ) + + alice.rpc.assertHospitalCountsAllZero() + assertEquals(0, alice.rpc.stateMachinesSnapshot().size) + alice.rpc.assertNumberOfCheckpoints(0) + } + } + + /** + * Throws an exception when performing an [Action.CommitTransaction] event when trying to propagate an error (processing an + * [Event.StartErrorPropagation] event) + * + * The exception is thrown 3 times. + * + * This causes the flow to retry the [Event.StartErrorPropagation] event until it succeeds. This this scenario it is retried 3 times, + * on the final retry the flow successfully propagates the error and completes exceptionally. + */ + @Test(timeout = 300_000) + fun `error during error propagation the flow is able to retry and recover`() { + startDriver { + val (alice, port) = createBytemanNode(ALICE_NAME) + + val rules = """ + RULE Create Counter + CLASS ${ThrowAnErrorFlow::class.java.name} + METHOD throwException + AT ENTRY + IF !flagged("my_flag") + DO traceln("SETTING FLAG TO TRUE"); flag("my_flag") + ENDRULE + + RULE Throw exception on executeCommitTransaction action + CLASS ${ActionExecutorImpl::class.java.name} + METHOD executeCommitTransaction + AT ENTRY + IF flagged("my_flag") && readCounter("counter") < 3 + DO traceln("Throwing exception"); incrementCounter("counter"); throw new java.sql.SQLException("die dammit die", "1") + ENDRULE + """.trimIndent() + + submitBytemanRules(rules, port) + + assertFailsWith { + alice.rpc.startFlow(StateMachineErrorHandlingTest::ThrowAnErrorFlow).returnValue.getOrThrow(60.seconds) + } + + alice.rpc.assertHospitalCounts( + propagated = 1, + propagatedRetry = 3 + ) + assertEquals(0, alice.rpc.stateMachinesSnapshot().size) + alice.rpc.assertNumberOfCheckpoints(0) + } + } + + /** + * Throws an exception when replaying a flow that has already successfully created its initial checkpoint. + * + * An exception is thrown when committing a database transaction during a transition to trigger the retry of the flow. Another + * exception is then thrown during the retry itself. + * + * The flow is discharged and replayed from the hospital. An exception is then thrown during the retry that causes the flow to be + * retried again. + */ + @Test(timeout = 300_000) + fun `error during flow retry when executing retryFlowFromSafePoint the flow is able to retry and recover`() { + startDriver { + val charlie = createNode(CHARLIE_NAME) + val (alice, port) = createBytemanNode(ALICE_NAME) + + val rules = """ + RULE Set flag when executing first suspend + CLASS ${TopLevelTransition::class.java.name} + METHOD suspendTransition + AT ENTRY + IF !flagged("suspend_flag") + DO flag("suspend_flag"); traceln("Setting suspend flag to true") + ENDRULE + + RULE Throw exception on executeCommitTransaction action after first suspend + commit + CLASS ${ActionExecutorImpl::class.java.name} + METHOD executeCommitTransaction + AT ENTRY + IF flagged("suspend_flag") && flagged("commit_flag") && !flagged("commit_exception_flag") + DO flag("commit_exception_flag"); traceln("Throwing exception"); throw new java.sql.SQLException("die dammit die", "1") + ENDRULE + + RULE Set flag when executing first commit + CLASS ${ActionExecutorImpl::class.java.name} + METHOD executeCommitTransaction + AT ENTRY + IF flagged("suspend_flag") && !flagged("commit_flag") + DO flag("commit_flag"); traceln("Setting commit flag to true") + ENDRULE + + RULE Throw exception on retry + CLASS ${SingleThreadedStateMachineManager::class.java.name} + METHOD addAndStartFlow + AT ENTRY + IF flagged("suspend_flag") && flagged("commit_flag") && !flagged("retry_exception_flag") + DO flag("retry_exception_flag"); traceln("Throwing retry exception"); throw new java.lang.RuntimeException("Here we go again") + ENDRULE + """.trimIndent() + + submitBytemanRules(rules, port) + + alice.rpc.startFlow( + StateMachineErrorHandlingTest::SendAMessageFlow, + charlie.nodeInfo.singleIdentity() + ).returnValue.getOrThrow(40.seconds) + + alice.rpc.assertHospitalCounts( + discharged = 1, + dischargedRetry = 1 + ) + assertEquals(0, alice.rpc.stateMachinesSnapshot().size) + alice.rpc.assertNumberOfCheckpoints(0) + } + } + + /** + * Throws an exception when performing an [Action.CommitTransaction] event after the flow has suspended (has moved to a started state). + * + * The exception is thrown 3 times. + * + * This causes the transition to be discharged from the hospital 3 times (retries 3 times). On the final retry the transition + * succeeds and the flow finishes. + * + * Each time the flow retries, it begins from the previous checkpoint where it suspended before failing. + */ + @Test(timeout = 300_000) + fun `error during transition with CommitTransaction action that occurs after the first suspend will retry and complete successfully`() { + startDriver { + val charlie = createNode(CHARLIE_NAME) + val (alice, port) = createBytemanNode(ALICE_NAME) + + // seems to be restarting the flow from the beginning every time + val rules = """ + RULE Create Counter + CLASS ${ActionExecutorImpl::class.java.name} + METHOD executeCommitTransaction + AT ENTRY + IF createCounter("counter", $counter) + DO traceln("Counter created") + ENDRULE + + RULE Set flag when executing first suspend + CLASS ${TopLevelTransition::class.java.name} + METHOD suspendTransition + AT ENTRY + IF !flagged("suspend_flag") + DO flag("suspend_flag"); traceln("Setting suspend flag to true") + ENDRULE + + RULE Throw exception on executeCommitTransaction action after first suspend + commit + CLASS ${ActionExecutorImpl::class.java.name} + METHOD executeCommitTransaction + AT ENTRY + IF flagged("suspend_flag") && flagged("commit_flag") && readCounter("counter") < 3 + DO incrementCounter("counter"); traceln("Throwing exception"); throw new java.lang.RuntimeException("die dammit die") + ENDRULE + + RULE Set flag when executing first commit + CLASS ${ActionExecutorImpl::class.java.name} + METHOD executeCommitTransaction + AT ENTRY + IF flagged("suspend_flag") && !flagged("commit_flag") + DO flag("commit_flag"); traceln("Setting commit flag to true") + ENDRULE + """.trimIndent() + + submitBytemanRules(rules, port) + + alice.rpc.startFlow( + StateMachineErrorHandlingTest::SendAMessageFlow, + charlie.nodeInfo.singleIdentity() + ).returnValue.getOrThrow( + 30.seconds + ) + + alice.rpc.assertHospitalCounts(discharged = 3) + assertEquals(0, alice.rpc.stateMachinesSnapshot().size) + alice.rpc.assertNumberOfCheckpoints(0) + } + } + + /** + * Throws an exception when performing an [Action.CommitTransaction] event when the flow is finishing. + * + * The exception is thrown 3 times. + * + * This causes the transition to be discharged from the hospital 3 times (retries 3 times). On the final retry the transition + * succeeds and the flow finishes. + * + * Each time the flow retries, it begins from the previous checkpoint where it suspended before failing. + */ + @Test(timeout = 300_000) + fun `error during transition with CommitTransaction action that occurs when completing a flow and deleting its checkpoint will retry and complete successfully`() { + startDriver { + val charlie = createNode(CHARLIE_NAME) + val (alice, port) = createBytemanNode(ALICE_NAME) + + // seems to be restarting the flow from the beginning every time + val rules = """ + RULE Create Counter + CLASS ${ActionExecutorImpl::class.java.name} + METHOD executeCommitTransaction + AT ENTRY + IF createCounter("counter", $counter) + DO traceln("Counter created") + ENDRULE + + RULE Set flag when adding action to remove checkpoint + CLASS ${TopLevelTransition::class.java.name} + METHOD flowFinishTransition + AT ENTRY + IF !flagged("remove_checkpoint_flag") + DO flag("remove_checkpoint_flag"); traceln("Setting remove checkpoint flag to true") + ENDRULE + + RULE Throw exception on executeCommitTransaction when removing checkpoint + CLASS ${ActionExecutorImpl::class.java.name} + METHOD executeCommitTransaction + AT ENTRY + IF flagged("remove_checkpoint_flag") && readCounter("counter") < 3 + DO incrementCounter("counter"); clear("remove_checkpoint_flag"); traceln("Throwing exception"); throw new java.sql.SQLException("die dammit die", "1") + ENDRULE + """.trimIndent() + + submitBytemanRules(rules, port) + + alice.rpc.startFlow( + StateMachineErrorHandlingTest::SendAMessageFlow, + charlie.nodeInfo.singleIdentity() + ).returnValue.getOrThrow( + 30.seconds + ) + + alice.rpc.assertHospitalCounts(discharged = 3) + assertEquals(0, alice.rpc.stateMachinesSnapshot().size) + alice.rpc.assertNumberOfCheckpoints(0) + } + } + + /** + * Throws a [ConstraintViolationException] when performing an [Action.CommitTransaction] event when the flow is finishing. + * + * The exception is thrown 4 times. + * + * This causes the transition to be discharged from the hospital 3 times (retries 3 times) and then be kept in for observation. + * + * Each time the flow retries, it begins from the previous checkpoint where it suspended before failing. + */ + @Test(timeout = 300_000) + fun `error during transition with CommitTransaction action and ConstraintViolationException that occurs when completing a flow will retry and be kept for observation if error persists`() { + startDriver { + val charlie = createNode(CHARLIE_NAME) + val (alice, port) = createBytemanNode(ALICE_NAME) + + val rules = """ + RULE Create Counter + CLASS ${ActionExecutorImpl::class.java.name} + METHOD executeCommitTransaction + AT ENTRY + IF createCounter("counter", $counter) + DO traceln("Counter created") + ENDRULE + + RULE Set flag when adding action to remove checkpoint + CLASS ${TopLevelTransition::class.java.name} + METHOD flowFinishTransition + AT ENTRY + IF !flagged("remove_checkpoint_flag") + DO flag("remove_checkpoint_flag"); traceln("Setting remove checkpoint flag to true") + ENDRULE + + RULE Throw exception on executeCommitTransaction when removing checkpoint + CLASS ${ActionExecutorImpl::class.java.name} + METHOD executeCommitTransaction + AT ENTRY + IF flagged("remove_checkpoint_flag") && readCounter("counter") < 4 + DO incrementCounter("counter"); + clear("remove_checkpoint_flag"); + traceln("Throwing exception"); + throw new org.hibernate.exception.ConstraintViolationException("This flow has a terminal condition", new java.sql.SQLException(), "made up constraint") + ENDRULE + """.trimIndent() + + submitBytemanRules(rules, port) + + assertFailsWith { + alice.rpc.startFlow( + StateMachineErrorHandlingTest::SendAMessageFlow, + charlie.nodeInfo.singleIdentity() + ).returnValue.getOrThrow( + 30.seconds + ) + } + + alice.rpc.assertHospitalCounts( + discharged = 3, + observation = 1 + ) + assertEquals(1, alice.rpc.stateMachinesSnapshot().size) + alice.rpc.assertNumberOfCheckpoints(1) + } + } + + /** + * Throws an exception when performing an [Action.CommitTransaction] event when the flow is finishing on a responding node. + * + * The exception is thrown 3 times. + * + * This causes the transition to be discharged from the hospital 3 times (retries 3 times). On the final retry the transition + * succeeds and the flow finishes. + */ + @Test(timeout = 300_000) + fun `responding flow - error during transition with CommitTransaction action that occurs when completing a flow and deleting its checkpoint will retry and complete successfully`() { + startDriver { + val (charlie, port) = createBytemanNode(CHARLIE_NAME) + val alice = createNode(ALICE_NAME) + + val rules = """ + RULE Create Counter + CLASS ${ActionExecutorImpl::class.java.name} + METHOD executeCommitTransaction + AT ENTRY + IF createCounter("counter", $counter) + DO traceln("Counter created") + ENDRULE + + RULE Set flag when adding action to remove checkpoint + CLASS ${TopLevelTransition::class.java.name} + METHOD flowFinishTransition + AT ENTRY + IF !flagged("remove_checkpoint_flag") + DO flag("remove_checkpoint_flag"); traceln("Setting remove checkpoint flag to true") + ENDRULE + + RULE Throw exception on executeCommitTransaction when removing checkpoint + CLASS ${ActionExecutorImpl::class.java.name} + METHOD executeCommitTransaction + AT ENTRY + IF flagged("remove_checkpoint_flag") && readCounter("counter") < 3 + DO incrementCounter("counter"); + clear("remove_checkpoint_flag"); + traceln("Throwing exception"); + throw new java.sql.SQLException("die dammit die", "1") + ENDRULE + """.trimIndent() + + submitBytemanRules(rules, port) + + alice.rpc.startFlow( + StateMachineErrorHandlingTest::SendAMessageFlow, + charlie.nodeInfo.singleIdentity() + ).returnValue.getOrThrow( + 30.seconds + ) + + charlie.rpc.assertHospitalCounts(discharged = 3) + assertEquals(0, alice.rpc.stateMachinesSnapshot().size) + assertEquals(0, charlie.rpc.stateMachinesSnapshot().size) + alice.rpc.assertNumberOfCheckpoints(0) + charlie.rpc.assertNumberOfCheckpoints(0) + } + } +} \ No newline at end of file diff --git a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineKillFlowErrorHandlingTest.kt b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineKillFlowErrorHandlingTest.kt new file mode 100644 index 0000000000..ae07eb60d4 --- /dev/null +++ b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineKillFlowErrorHandlingTest.kt @@ -0,0 +1,196 @@ +package net.corda.node.services.statemachine + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.StartableByRPC +import net.corda.core.messaging.startFlow +import net.corda.core.messaging.startTrackedFlow +import net.corda.core.utilities.ProgressTracker +import net.corda.core.utilities.getOrThrow +import net.corda.core.utilities.seconds +import net.corda.testing.core.ALICE_NAME +import net.corda.testing.core.CHARLIE_NAME +import net.corda.testing.core.singleIdentity +import org.junit.Test +import java.time.Duration +import java.time.temporal.ChronoUnit +import java.util.concurrent.TimeoutException +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue + +@Suppress("MaxLineLength") // Byteman rules cannot be easily wrapped +class StateMachineKillFlowErrorHandlingTest : StateMachineErrorHandlingTest() { + + /** + * Triggers `killFlow` while the flow is suspended causing a [InterruptedException] to be thrown and passed through the hospital. + * + * The flow terminates and is not retried. + * + * No pass through the hospital is recorded. As the flow is marked as `isRemoved`. + */ + @Test(timeout = 300_000) + fun `error during transition due to an InterruptedException (killFlow) will terminate the flow`() { + startDriver { + val (alice, port) = createBytemanNode(ALICE_NAME) + + val rules = """ + RULE Increment terminal counter + CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} + METHOD consult + AT READ TERMINAL + IF true + DO traceln("Byteman test - terminal") + ENDRULE + """.trimIndent() + + submitBytemanRules(rules, port) + + val flow = alice.rpc.startTrackedFlow(StateMachineKillFlowErrorHandlingTest::SleepFlow) + + var flowKilled = false + flow.progress.subscribe { + if (it == SleepFlow.STARTED.label) { + Thread.sleep(5000) + flowKilled = alice.rpc.killFlow(flow.id) + } + } + + assertFailsWith { flow.returnValue.getOrThrow(20.seconds) } + + val output = getBytemanOutput(alice) + + assertTrue(flowKilled) + val numberOfTerminalDiagnoses = output.filter { it.contains("Byteman test - terminal") }.size + assertEquals(1, numberOfTerminalDiagnoses) + alice.rpc.assertHospitalCounts(propagated = 1) + assertEquals(0, alice.rpc.stateMachinesSnapshot().size) + alice.rpc.assertNumberOfCheckpoints(0) + } + } + + /** + * Triggers `killFlow` during user application code. + * + * The user application code is mimicked by a [Thread.sleep] which is importantly not placed inside the [Suspendable] + * call function. Placing it inside a [Suspendable] function causes quasar to behave unexpectedly. + * + * Although the call to kill the flow is made during user application code. It will not be removed / stop processing + * until the next suspension point is reached within the flow. + * + * The flow terminates and is not retried. + * + * No pass through the hospital is recorded. As the flow is marked as `isRemoved`. + */ + @Test(timeout = 300_000) + fun `flow killed during user code execution stops and removes the flow correctly`() { + startDriver { + val alice = createNode(ALICE_NAME) + + val flow = alice.rpc.startTrackedFlow(StateMachineKillFlowErrorHandlingTest::ThreadSleepFlow) + + var flowKilled = false + flow.progress.subscribe { + if (it == ThreadSleepFlow.STARTED.label) { + Thread.sleep(5000) + flowKilled = alice.rpc.killFlow(flow.id) + } + } + + assertFailsWith { flow.returnValue.getOrThrow(30.seconds) } + + assertTrue(flowKilled) + alice.rpc.assertHospitalCountsAllZero() + assertEquals(0, alice.rpc.stateMachinesSnapshot().size) + alice.rpc.assertNumberOfCheckpoints(0) + } + } + + /** + * Triggers `killFlow` after the flow has already been sent to observation. The flow is not running at this point and + * all that remains is its checkpoint in the database. + * + * The flow terminates and is not retried. + * + * Killing the flow does not lead to any passes through the hospital. All the recorded passes through the hospital are + * from the original flow that was put in for observation. + */ + @Test(timeout = 300_000) + fun `flow killed when it is in the flow hospital for observation is removed correctly`() { + startDriver { + val (alice, port) = createBytemanNode(ALICE_NAME) + val charlie = createNode(CHARLIE_NAME) + + val rules = """ + RULE Create Counter + CLASS ${ActionExecutorImpl::class.java.name} + METHOD executeSendInitial + AT ENTRY + IF createCounter("counter", $counter) + DO traceln("Counter created") + ENDRULE + + RULE Throw exception on executeSendInitial action + CLASS ${ActionExecutorImpl::class.java.name} + METHOD executeSendInitial + AT ENTRY + IF readCounter("counter") < 4 + DO incrementCounter("counter"); traceln("Throwing exception"); throw new java.lang.RuntimeException("die dammit die") + ENDRULE + """.trimIndent() + + submitBytemanRules(rules, port) + + val flow = alice.rpc.startFlow(StateMachineErrorHandlingTest::SendAMessageFlow, charlie.nodeInfo.singleIdentity()) + + assertFailsWith { flow.returnValue.getOrThrow(20.seconds) } + + alice.rpc.killFlow(flow.id) + + alice.rpc.assertHospitalCounts( + discharged = 3, + observation = 1 + ) + assertEquals(0, alice.rpc.stateMachinesSnapshot().size) + alice.rpc.assertNumberOfCheckpoints(0) + } + } + + @StartableByRPC + class SleepFlow : FlowLogic() { + + object STARTED : ProgressTracker.Step("I am ready to die") + + override val progressTracker = ProgressTracker(STARTED) + + @Suspendable + override fun call() { + sleep(Duration.of(1, ChronoUnit.SECONDS)) + progressTracker.currentStep = STARTED + sleep(Duration.of(2, ChronoUnit.MINUTES)) + } + } + + @StartableByRPC + class ThreadSleepFlow : FlowLogic() { + + object STARTED : ProgressTracker.Step("I am ready to die") + + override val progressTracker = ProgressTracker(STARTED) + + @Suspendable + override fun call() { + sleep(Duration.of(1, ChronoUnit.SECONDS)) + progressTracker.currentStep = STARTED + logger.info("Starting ${ThreadSleepFlow::class.qualifiedName} application sleep") + sleep() + logger.info("Finished ${ThreadSleepFlow::class.qualifiedName} application sleep") + sleep(Duration.of(2, ChronoUnit.MINUTES)) + } + + // Sleep is moved outside of `@Suspendable` function to prevent issues with Quasar + private fun sleep() { + Thread.sleep(20000) + } + } +} \ No newline at end of file diff --git a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineSubflowErrorHandlingTest.kt b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineSubFlowErrorHandlingTest.kt similarity index 60% rename from node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineSubflowErrorHandlingTest.kt rename to node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineSubFlowErrorHandlingTest.kt index fd491eab97..02757dca25 100644 --- a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineSubflowErrorHandlingTest.kt +++ b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineSubFlowErrorHandlingTest.kt @@ -1,7 +1,6 @@ package net.corda.node.services.statemachine import co.paralleluniverse.fibers.Suspendable -import net.corda.client.rpc.CordaRPCClient import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowSession import net.corda.core.flows.InitiatedBy @@ -20,13 +19,14 @@ import org.junit.Test import kotlin.test.assertEquals @Suppress("MaxLineLength") // Byteman rules cannot be easily wrapped -class StatemachineSubflowErrorHandlingTest : StatemachineErrorHandlingTest() { +class StateMachineSubFlowErrorHandlingTest : StateMachineErrorHandlingTest() { /** * This test checks that flow calling an initiating subflow will recover correctly. * * Throws an exception when performing an [Action.CommitTransaction] event during the subflow's first send to a counterparty. - * The exception is thrown 5 times. + * + * The exception is thrown 3 times. * * This causes the transition to be discharged from the hospital 3 times (retries 3 times). On the final retry the transition * succeeds and the flow finishes. @@ -37,11 +37,11 @@ class StatemachineSubflowErrorHandlingTest : StatemachineErrorHandlingTest() { * if an error transition moves into another error transition. The flow still recovers from this state. 5 exceptions were thrown to verify * that 3 retries are attempted before recovering. */ - @Test(timeout=300_000) - fun `initiating subflow - error during transition with CommitTransaction action that occurs during the first send will retry and complete successfully`() { + @Test(timeout = 300_000) + fun `initiating subflow - error during transition with CommitTransaction action that occurs during the first send will retry and complete successfully`() { startDriver { val charlie = createNode(CHARLIE_NAME) - val alice = createBytemanNode(ALICE_NAME) + val (alice, port) = createBytemanNode(ALICE_NAME) val rules = """ RULE Create Counter @@ -72,7 +72,7 @@ class StatemachineSubflowErrorHandlingTest : StatemachineErrorHandlingTest() { CLASS ${ActionExecutorImpl::class.java.name} METHOD executeCommitTransaction AT ENTRY - IF flagged("subflow_flag") && flagged("suspend_flag") && flagged("commit_flag") && readCounter("counter") < 5 + IF flagged("subflow_flag") && flagged("suspend_flag") && flagged("commit_flag") && readCounter("counter") < 3 DO incrementCounter("counter"); traceln("Throwing exception"); throw new java.lang.RuntimeException("die dammit die") ENDRULE @@ -83,52 +83,20 @@ class StatemachineSubflowErrorHandlingTest : StatemachineErrorHandlingTest() { IF flagged("subflow_flag") && flagged("suspend_flag") && !flagged("commit_flag") DO flag("commit_flag"); traceln("Setting commit flag to true") ENDRULE - - RULE Entering internal error staff member - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT ENTRY - IF true - DO traceln("Reached internal transition error staff member") - ENDRULE - - RULE Increment discharge counter - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT READ DISCHARGE - IF true - DO traceln("Byteman test - discharging") - ENDRULE - - RULE Increment observation counter - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT READ OVERNIGHT_OBSERVATION - IF true - DO traceln("Byteman test - overnight observation") - ENDRULE """.trimIndent() - submitBytemanRules(rules) + submitBytemanRules(rules, port) - val aliceClient = - CordaRPCClient(alice.rpcAddress).start(rpcUser.username, rpcUser.password).proxy - - aliceClient.startFlow(StatemachineSubflowErrorHandlingTest::SendAMessageInAnInitiatingSubflowFlow, charlie.nodeInfo.singleIdentity()).returnValue.getOrThrow( + alice.rpc.startFlow( + StateMachineSubFlowErrorHandlingTest::SendAMessageInAnInitiatingSubflowFlow, + charlie.nodeInfo.singleIdentity() + ).returnValue.getOrThrow( 30.seconds ) - val output = getBytemanOutput(alice) - - // Check the stdout for the lines generated by byteman - assertEquals(3, output.filter { it.contains("Byteman test - discharging") }.size) - assertEquals(0, output.filter { it.contains("Byteman test - overnight observation") }.size) - val (discharge, observation) = aliceClient.startFlow(StatemachineErrorHandlingTest::GetHospitalCountersFlow).returnValue.get() - assertEquals(3, discharge) - assertEquals(0, observation) - assertEquals(0, aliceClient.stateMachinesSnapshot().size) - // 1 for GetNumberOfCheckpointsFlow - assertEquals(1, aliceClient.startFlow(StatemachineErrorHandlingTest::GetNumberOfCheckpointsFlow).returnValue.get()) + alice.rpc.assertHospitalCounts(discharged = 3) + assertEquals(0, alice.rpc.stateMachinesSnapshot().size) + alice.rpc.assertNumberOfCheckpoints(0) } } @@ -136,7 +104,8 @@ class StatemachineSubflowErrorHandlingTest : StatemachineErrorHandlingTest() { * This test checks that flow calling an initiating subflow will recover correctly. * * Throws an exception when performing an [Action.CommitTransaction] event during the subflow's first receive from a counterparty. - * The exception is thrown 5 times. + * + * The exception is thrown 3 times. * * This causes the transition to be discharged from the hospital 3 times (retries 3 times). On the final retry the transition * succeeds and the flow finishes. @@ -147,11 +116,11 @@ class StatemachineSubflowErrorHandlingTest : StatemachineErrorHandlingTest() { * if an error transition moves into another error transition. The flow still recovers from this state. 5 exceptions were thrown to verify * that 3 retries are attempted before recovering. */ - @Test(timeout=300_000) - fun `initiating subflow - error during transition with CommitTransaction action that occurs after the first receive will retry and complete successfully`() { + @Test(timeout = 300_000) + fun `initiating subflow - error during transition with CommitTransaction action that occurs after the first receive will retry and complete successfully`() { startDriver { val charlie = createNode(CHARLIE_NAME) - val alice = createBytemanNode(ALICE_NAME) + val (alice, port) = createBytemanNode(ALICE_NAME) val rules = """ RULE Create Counter @@ -182,55 +151,23 @@ class StatemachineSubflowErrorHandlingTest : StatemachineErrorHandlingTest() { CLASS ${ActionExecutorImpl::class.java.name} METHOD executeCommitTransaction AT ENTRY - IF flagged("subflow_flag") && flagged("suspend_flag") && readCounter("counter") < 5 + IF flagged("subflow_flag") && flagged("suspend_flag") && readCounter("counter") < 3 DO incrementCounter("counter"); traceln("Throwing exception"); throw new java.lang.RuntimeException("die dammit die") ENDRULE - - RULE Entering internal error staff member - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT ENTRY - IF true - DO traceln("Reached internal transition error staff member") - ENDRULE - - RULE Increment discharge counter - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT READ DISCHARGE - IF true - DO traceln("Byteman test - discharging") - ENDRULE - - RULE Increment observation counter - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT READ OVERNIGHT_OBSERVATION - IF true - DO traceln("Byteman test - overnight observation") - ENDRULE """.trimIndent() - submitBytemanRules(rules) + submitBytemanRules(rules, port) - val aliceClient = - CordaRPCClient(alice.rpcAddress).start(rpcUser.username, rpcUser.password).proxy - - aliceClient.startFlow(StatemachineSubflowErrorHandlingTest::SendAMessageInAnInitiatingSubflowFlow, charlie.nodeInfo.singleIdentity()).returnValue.getOrThrow( + alice.rpc.startFlow( + StateMachineSubFlowErrorHandlingTest::SendAMessageInAnInitiatingSubflowFlow, + charlie.nodeInfo.singleIdentity() + ).returnValue.getOrThrow( 30.seconds ) - val output = getBytemanOutput(alice) - - // Check the stdout for the lines generated by byteman - assertEquals(3, output.filter { it.contains("Byteman test - discharging") }.size) - assertEquals(0, output.filter { it.contains("Byteman test - overnight observation") }.size) - val (discharge, observation) = aliceClient.startFlow(StatemachineErrorHandlingTest::GetHospitalCountersFlow).returnValue.get() - assertEquals(3, discharge) - assertEquals(0, observation) - assertEquals(0, aliceClient.stateMachinesSnapshot().size) - // 1 for GetNumberOfCheckpointsFlow - assertEquals(1, aliceClient.startFlow(StatemachineErrorHandlingTest::GetNumberOfCheckpointsFlow).returnValue.get()) + alice.rpc.assertHospitalCounts(discharged = 3) + assertEquals(0, alice.rpc.stateMachinesSnapshot().size) + alice.rpc.assertNumberOfCheckpoints(0) } } @@ -238,7 +175,8 @@ class StatemachineSubflowErrorHandlingTest : StatemachineErrorHandlingTest() { * This test checks that flow calling an inline subflow will recover correctly. * * Throws an exception when performing an [Action.CommitTransaction] event during the subflow's first send to a counterparty. - * The exception is thrown 5 times. + * + * The exception is thrown 3 times. * * This causes the transition to be discharged from the hospital 3 times (retries 3 times). On the final retry the transition * succeeds and the flow finishes. @@ -249,11 +187,11 @@ class StatemachineSubflowErrorHandlingTest : StatemachineErrorHandlingTest() { * if an error transition moves into another error transition. The flow still recovers from this state. 5 exceptions were thrown to verify * that 3 retries are attempted before recovering. */ - @Test(timeout=300_000) - fun `inline subflow - error during transition with CommitTransaction action that occurs during the first send will retry and complete successfully`() { + @Test(timeout = 300_000) + fun `inline subflow - error during transition with CommitTransaction action that occurs during the first send will retry and complete successfully`() { startDriver { val charlie = createNode(CHARLIE_NAME) - val alice = createBytemanNode(ALICE_NAME) + val (alice, port) = createBytemanNode(ALICE_NAME) val rules = """ RULE Create Counter @@ -276,55 +214,23 @@ class StatemachineSubflowErrorHandlingTest : StatemachineErrorHandlingTest() { CLASS ${ActionExecutorImpl::class.java.name} METHOD executeCommitTransaction AT ENTRY - IF flagged("subflow_flag") && readCounter("counter") < 5 + IF flagged("subflow_flag") && readCounter("counter") < 3 DO incrementCounter("counter"); traceln("Throwing exception"); throw new java.lang.RuntimeException("die dammit die") ENDRULE - - RULE Entering internal error staff member - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT ENTRY - IF true - DO traceln("Reached internal transition error staff member") - ENDRULE - - RULE Increment discharge counter - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT READ DISCHARGE - IF true - DO traceln("Byteman test - discharging") - ENDRULE - - RULE Increment observation counter - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT READ OVERNIGHT_OBSERVATION - IF true - DO traceln("Byteman test - overnight observation") - ENDRULE """.trimIndent() - submitBytemanRules(rules) + submitBytemanRules(rules, port) - val aliceClient = - CordaRPCClient(alice.rpcAddress).start(rpcUser.username, rpcUser.password).proxy - - aliceClient.startFlow(StatemachineSubflowErrorHandlingTest::SendAMessageInAnInlineSubflowFlow, charlie.nodeInfo.singleIdentity()).returnValue.getOrThrow( + alice.rpc.startFlow( + StateMachineSubFlowErrorHandlingTest::SendAMessageInAnInlineSubflowFlow, + charlie.nodeInfo.singleIdentity() + ).returnValue.getOrThrow( 30.seconds ) - val output = getBytemanOutput(alice) - - // Check the stdout for the lines generated by byteman - assertEquals(3, output.filter { it.contains("Byteman test - discharging") }.size) - assertEquals(0, output.filter { it.contains("Byteman test - overnight observation") }.size) - val (discharge, observation) = aliceClient.startFlow(StatemachineErrorHandlingTest::GetHospitalCountersFlow).returnValue.get() - assertEquals(3, discharge) - assertEquals(0, observation) - assertEquals(0, aliceClient.stateMachinesSnapshot().size) - // 1 for GetNumberOfCheckpointsFlow - assertEquals(1, aliceClient.startFlow(StatemachineErrorHandlingTest::GetNumberOfCheckpointsFlow).returnValue.get()) + alice.rpc.assertHospitalCounts(discharged = 3) + assertEquals(0, alice.rpc.stateMachinesSnapshot().size) + alice.rpc.assertNumberOfCheckpoints(0) } } @@ -332,7 +238,8 @@ class StatemachineSubflowErrorHandlingTest : StatemachineErrorHandlingTest() { * This test checks that flow calling an inline subflow will recover correctly. * * Throws an exception when performing an [Action.CommitTransaction] event during the subflow's first receive from a counterparty. - * The exception is thrown 5 times. + * + * The exception is thrown 3 times. * * This causes the transition to be discharged from the hospital 3 times (retries 3 times). On the final retry the transition * succeeds and the flow finishes. @@ -343,11 +250,11 @@ class StatemachineSubflowErrorHandlingTest : StatemachineErrorHandlingTest() { * if an error transition moves into another error transition. The flow still recovers from this state. 5 exceptions were thrown to verify * that 3 retries are attempted before recovering. */ - @Test(timeout=300_000) - fun `inline subflow - error during transition with CommitTransaction action that occurs during the first receive will retry and complete successfully`() { + @Test(timeout = 300_000) + fun `inline subflow - error during transition with CommitTransaction action that occurs during the first receive will retry and complete successfully`() { startDriver { val charlie = createNode(CHARLIE_NAME) - val alice = createBytemanNode(ALICE_NAME) + val (alice, port) = createBytemanNode(ALICE_NAME) val rules = """ RULE Create Counter @@ -370,7 +277,7 @@ class StatemachineSubflowErrorHandlingTest : StatemachineErrorHandlingTest() { CLASS ${ActionExecutorImpl::class.java.name} METHOD executeCommitTransaction AT ENTRY - IF flagged("subflow_flag") && flagged("commit_flag") && readCounter("counter") < 5 + IF flagged("subflow_flag") && flagged("commit_flag") && readCounter("counter") < 3 DO incrementCounter("counter"); traceln("Throwing exception"); throw new java.lang.RuntimeException("die dammit die") ENDRULE @@ -381,52 +288,20 @@ class StatemachineSubflowErrorHandlingTest : StatemachineErrorHandlingTest() { IF flagged("subflow_flag") && !flagged("commit_flag") DO flag("commit_flag"); traceln("Setting commit flag to true") ENDRULE - - RULE Entering internal error staff member - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT ENTRY - IF true - DO traceln("Reached internal transition error staff member") - ENDRULE - - RULE Increment discharge counter - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT READ DISCHARGE - IF true - DO traceln("Byteman test - discharging") - ENDRULE - - RULE Increment observation counter - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT READ OVERNIGHT_OBSERVATION - IF true - DO traceln("Byteman test - overnight observation") - ENDRULE """.trimIndent() - submitBytemanRules(rules) + submitBytemanRules(rules, port) - val aliceClient = - CordaRPCClient(alice.rpcAddress).start(rpcUser.username, rpcUser.password).proxy - - aliceClient.startFlow(StatemachineSubflowErrorHandlingTest::SendAMessageInAnInlineSubflowFlow, charlie.nodeInfo.singleIdentity()).returnValue.getOrThrow( + alice.rpc.startFlow( + StateMachineSubFlowErrorHandlingTest::SendAMessageInAnInlineSubflowFlow, + charlie.nodeInfo.singleIdentity() + ).returnValue.getOrThrow( 30.seconds ) - val output = getBytemanOutput(alice) - - // Check the stdout for the lines generated by byteman - assertEquals(3, output.filter { it.contains("Byteman test - discharging") }.size) - assertEquals(0, output.filter { it.contains("Byteman test - overnight observation") }.size) - val (discharge, observation) = aliceClient.startFlow(StatemachineErrorHandlingTest::GetHospitalCountersFlow).returnValue.get() - assertEquals(3, discharge) - assertEquals(0, observation) - assertEquals(0, aliceClient.stateMachinesSnapshot().size) - // 1 for GetNumberOfCheckpointsFlow - assertEquals(1, aliceClient.startFlow(StatemachineErrorHandlingTest::GetNumberOfCheckpointsFlow).returnValue.get()) + alice.rpc.assertHospitalCounts(discharged = 3) + assertEquals(0, alice.rpc.stateMachinesSnapshot().size) + alice.rpc.assertNumberOfCheckpoints(0) } } diff --git a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineGeneralErrorHandlingTest.kt b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineGeneralErrorHandlingTest.kt deleted file mode 100644 index 8308328827..0000000000 --- a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineGeneralErrorHandlingTest.kt +++ /dev/null @@ -1,1281 +0,0 @@ -package net.corda.node.services.statemachine - -import net.corda.client.rpc.CordaRPCClient -import net.corda.core.messaging.startFlow -import net.corda.core.utilities.getOrThrow -import net.corda.core.utilities.seconds -import net.corda.node.services.messaging.DeduplicationHandler -import net.corda.node.services.statemachine.transitions.TopLevelTransition -import net.corda.testing.core.ALICE_NAME -import net.corda.testing.core.CHARLIE_NAME -import net.corda.testing.core.singleIdentity -import org.junit.Ignore -import org.junit.Test -import java.util.concurrent.TimeoutException -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith - -@Suppress("MaxLineLength") // Byteman rules cannot be easily wrapped -class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { - - /** - * Throws an exception when performing an [Action.SendInitial] action. - * The exception is thrown 4 times. - * - * This causes the transition to be discharged from the hospital 3 times (retries 3 times) and is then kept in - * the hospital for observation. - */ - @Test(timeout=300_000) - fun `error during transition with SendInitial action is retried 3 times and kept for observation if error persists`() { - startDriver { - val charlie = createNode(CHARLIE_NAME) - val alice = createBytemanNode(ALICE_NAME) - - val rules = """ - RULE Create Counter - CLASS ${ActionExecutorImpl::class.java.name} - METHOD executeSendInitial - AT ENTRY - IF createCounter("counter", $counter) - DO traceln("Counter created") - ENDRULE - - RULE Throw exception on executeSendInitial action - CLASS ${ActionExecutorImpl::class.java.name} - METHOD executeSendInitial - AT ENTRY - IF readCounter("counter") < 4 - DO incrementCounter("counter"); traceln("Throwing exception"); throw new java.lang.RuntimeException("die dammit die") - ENDRULE - - RULE Entering internal error staff member - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT ENTRY - IF true - DO traceln("Reached internal transition error staff member") - ENDRULE - - RULE Increment discharge counter - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT READ DISCHARGE - IF true - DO traceln("Byteman test - discharging") - ENDRULE - - RULE Increment observation counter - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT READ OVERNIGHT_OBSERVATION - IF true - DO traceln("Byteman test - overnight observation") - ENDRULE - """.trimIndent() - - submitBytemanRules(rules) - - val aliceClient = - CordaRPCClient(alice.rpcAddress).start(rpcUser.username, rpcUser.password).proxy - - assertFailsWith { - aliceClient.startFlow(StatemachineErrorHandlingTest::SendAMessageFlow, charlie.nodeInfo.singleIdentity()).returnValue.getOrThrow( - 30.seconds - ) - } - - val output = getBytemanOutput(alice) - - // Check the stdout for the lines generated by byteman - assertEquals(3, output.filter { it.contains("Byteman test - discharging") }.size) - assertEquals(1, output.filter { it.contains("Byteman test - overnight observation") }.size) - val (discharge, observation) = aliceClient.startFlow(StatemachineErrorHandlingTest::GetHospitalCountersFlow).returnValue.get() - assertEquals(3, discharge) - assertEquals(1, observation) - assertEquals(1, aliceClient.stateMachinesSnapshot().size) - // 1 for the errored flow kept for observation and another for GetNumberOfCheckpointsFlow - assertEquals(2, aliceClient.startFlow(StatemachineErrorHandlingTest::GetNumberOfCheckpointsFlow).returnValue.get()) - } - } - - /** - * Throws an exception when performing an [Action.SendInitial] event. - * The exception is thrown 3 times. - * - * This causes the transition to be discharged from the hospital 3 times (retries 3 times). On the final retry the transition - * succeeds and the flow finishes. - */ - @Test(timeout=300_000) - fun `error during transition with SendInitial action that does not persist will retry and complete successfully`() { - startDriver { - val charlie = createNode(CHARLIE_NAME) - val alice = createBytemanNode(ALICE_NAME) - - val rules = """ - RULE Create Counter - CLASS ${ActionExecutorImpl::class.java.name} - METHOD executeSendInitial - AT ENTRY - IF createCounter("counter", $counter) - DO traceln("Counter created") - ENDRULE - - RULE Throw exception on executeSendInitial action - CLASS ${ActionExecutorImpl::class.java.name} - METHOD executeSendInitial - AT ENTRY - IF readCounter("counter") < 3 - DO incrementCounter("counter"); traceln("Throwing exception"); throw new java.lang.RuntimeException("die dammit die") - ENDRULE - - RULE Entering internal error staff member - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT ENTRY - IF true - DO traceln("Reached internal transition error staff member") - ENDRULE - - RULE Increment discharge counter - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT READ DISCHARGE - IF true - DO traceln("Byteman test - discharging") - ENDRULE - - RULE Increment observation counter - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT READ OVERNIGHT_OBSERVATION - IF true - DO traceln("Byteman test - overnight observation") - ENDRULE - """.trimIndent() - - submitBytemanRules(rules) - - val aliceClient = - CordaRPCClient(alice.rpcAddress).start(rpcUser.username, rpcUser.password).proxy - - aliceClient.startFlow(StatemachineErrorHandlingTest::SendAMessageFlow, charlie.nodeInfo.singleIdentity()).returnValue.getOrThrow( - 30.seconds - ) - - val output = getBytemanOutput(alice) - - // Check the stdout for the lines generated by byteman - assertEquals(3, output.filter { it.contains("Byteman test - discharging") }.size) - assertEquals(0, output.filter { it.contains("Byteman test - overnight observation") }.size) - val (discharge, observation) = aliceClient.startFlow(StatemachineErrorHandlingTest::GetHospitalCountersFlow).returnValue.get() - assertEquals(3, discharge) - assertEquals(0, observation) - assertEquals(0, aliceClient.stateMachinesSnapshot().size) - // 1 for GetNumberOfCheckpointsFlow - assertEquals(1, aliceClient.startFlow(StatemachineErrorHandlingTest::GetNumberOfCheckpointsFlow).returnValue.get()) - } - } - - /** - * Throws an exception when executing [DeduplicationHandler.afterDatabaseTransaction] from - * inside an [Action.AcknowledgeMessages] action. - * The exception is thrown every time [DeduplicationHandler.afterDatabaseTransaction] is executed - * inside of [ActionExecutorImpl.executeAcknowledgeMessages] - * - * The exceptions should be swallowed. Therefore there should be no trips to the hospital and no retries. - * The flow should complete successfully as the error is swallowed. - */ - @Test(timeout=300_000) - fun `error during transition with AcknowledgeMessages action is swallowed and flow completes successfully`() { - startDriver { - val charlie = createNode(CHARLIE_NAME) - val alice = createBytemanNode(ALICE_NAME) - - val rules = """ - RULE Set flag when inside executeAcknowledgeMessages - CLASS ${ActionExecutorImpl::class.java.name} - METHOD executeAcknowledgeMessages - AT INVOKE ${DeduplicationHandler::class.java.name}.afterDatabaseTransaction() - IF !flagged("exception_flag") - DO flag("exception_flag"); traceln("Setting flag to true") - ENDRULE - - RULE Throw exception when executing ${DeduplicationHandler::class.java.name}.afterDatabaseTransaction when inside executeAcknowledgeMessages - INTERFACE ${DeduplicationHandler::class.java.name} - METHOD afterDatabaseTransaction - AT ENTRY - IF flagged("exception_flag") - DO traceln("Throwing exception"); clear("exception_flag"); traceln("SETTING FLAG TO FALSE"); throw new java.lang.RuntimeException("die dammit die") - ENDRULE - - RULE Entering internal error staff member - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT ENTRY - IF true - DO traceln("Reached internal transition error staff member") - ENDRULE - - RULE Increment discharge counter - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT READ DISCHARGE - IF true - DO traceln("Byteman test - discharging") - ENDRULE - - RULE Increment observation counter - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT READ OVERNIGHT_OBSERVATION - IF true - DO traceln("Byteman test - overnight observation") - ENDRULE - """.trimIndent() - - submitBytemanRules(rules) - - val aliceClient = - CordaRPCClient(alice.rpcAddress).start(rpcUser.username, rpcUser.password).proxy - - aliceClient.startFlow(StatemachineErrorHandlingTest::SendAMessageFlow, charlie.nodeInfo.singleIdentity()).returnValue.getOrThrow( - 30.seconds - ) - - val output = getBytemanOutput(alice) - - // Check the stdout for the lines generated by byteman - assertEquals(0, output.filter { it.contains("Byteman test - discharging") }.size) - assertEquals(0, output.filter { it.contains("Byteman test - overnight observation") }.size) - val (discharge, observation) = aliceClient.startFlow(StatemachineErrorHandlingTest::GetHospitalCountersFlow).returnValue.get() - assertEquals(0, discharge) - assertEquals(0, observation) - assertEquals(0, aliceClient.stateMachinesSnapshot().size) - // 1 for GetNumberOfCheckpointsFlow - assertEquals(1, aliceClient.startFlow(StatemachineErrorHandlingTest::GetNumberOfCheckpointsFlow).returnValue.get()) - } - } - - /** - * Throws an exception when performing an [Action.CommitTransaction] event before the flow has suspended (remains in an unstarted - * state). - * The exception is thrown 5 times. - * - * This causes the transition to be discharged from the hospital 3 times (retries 3 times). On the final retry the transition - * succeeds and the flow finishes. - * - * Each time the flow retries, it starts from the beginning of the flow (due to being in an unstarted state). - * - * 2 of the thrown exceptions are absorbed by the if statement in [TransitionExecutorImpl.executeTransition] that aborts the transition - * if an error transition moves into another error transition. The flow still recovers from this state. 5 exceptions were thrown to - * verify that 3 retries are attempted before recovering. - */ - @Test(timeout=300_000) - fun `error during transition with CommitTransaction action that occurs during the beginning of execution will retry and complete successfully`() { - startDriver { - val charlie = createNode(CHARLIE_NAME) - val alice = createBytemanNode(ALICE_NAME) - - val rules = """ - RULE Create Counter - CLASS ${ActionExecutorImpl::class.java.name} - METHOD executeCommitTransaction - AT ENTRY - IF createCounter("counter", $counter) - DO traceln("Counter created") - ENDRULE - - RULE Throw exception on executeCommitTransaction action - CLASS ${ActionExecutorImpl::class.java.name} - METHOD executeCommitTransaction - AT ENTRY - IF readCounter("counter") < 5 - DO incrementCounter("counter"); traceln("Throwing exception"); throw new java.lang.RuntimeException("die dammit die") - ENDRULE - - RULE Entering internal error staff member - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT ENTRY - IF true - DO traceln("Reached internal transition error staff member") - ENDRULE - - RULE Increment discharge counter - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT READ DISCHARGE - IF true - DO traceln("Byteman test - discharging") - ENDRULE - - RULE Increment observation counter - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT READ OVERNIGHT_OBSERVATION - IF true - DO traceln("Byteman test - overnight observation") - ENDRULE - """.trimIndent() - - submitBytemanRules(rules) - - val aliceClient = - CordaRPCClient(alice.rpcAddress).start(rpcUser.username, rpcUser.password).proxy - - aliceClient.startFlow(StatemachineErrorHandlingTest::SendAMessageFlow, charlie.nodeInfo.singleIdentity()).returnValue.getOrThrow( - 30.seconds - ) - - val output = getBytemanOutput(alice) - - // Check the stdout for the lines generated by byteman - assertEquals(3, output.filter { it.contains("Byteman test - discharging") }.size) - assertEquals(0, output.filter { it.contains("Byteman test - overnight observation") }.size) - val (discharge, observation) = aliceClient.startFlow(StatemachineErrorHandlingTest::GetHospitalCountersFlow).returnValue.get() - assertEquals(3, discharge) - assertEquals(0, observation) - assertEquals(0, aliceClient.stateMachinesSnapshot().size) - // 1 for GetNumberOfCheckpointsFlow - assertEquals(1, aliceClient.startFlow(StatemachineErrorHandlingTest::GetNumberOfCheckpointsFlow).returnValue.get()) - } - } - - /** - * Throws an exception when performing an [Action.CommitTransaction] event before the flow has suspended (remains in an unstarted - * state). - * The exception is thrown 7 times. - * - * This causes the transition to be discharged from the hospital 3 times (retries 3 times) and then be kept in for observation. - * - * Each time the flow retries, it starts from the beginning of the flow (due to being in an unstarted state). - * - * 2 of the thrown exceptions are absorbed by the if statement in [TransitionExecutorImpl.executeTransition] that aborts the transition - * if an error transition moves into another error transition. The flow still recovers from this state. 5 exceptions were thrown to - * verify that 3 retries are attempted before recovering. - * - * CORDA-3352 - it is currently hanging after putting the flow in for observation - */ - @Test(timeout=300_000) -@Ignore - fun `error during transition with CommitTransaction action that occurs during the beginning of execution will retry and be kept for observation if error persists`() { - startDriver { - val charlie = createNode(CHARLIE_NAME) - val alice = createBytemanNode(ALICE_NAME) - - val rules = """ - RULE Create Counter - CLASS ${ActionExecutorImpl::class.java.name} - METHOD executeCommitTransaction - AT ENTRY - IF createCounter("counter", $counter) - DO traceln("Counter created") - ENDRULE - - RULE Throw exception on executeCommitTransaction action - CLASS ${ActionExecutorImpl::class.java.name} - METHOD executeCommitTransaction - AT ENTRY - IF readCounter("counter") < 7 - DO incrementCounter("counter"); traceln("Throwing exception"); throw new java.lang.RuntimeException("die dammit die") - ENDRULE - - RULE Entering internal error staff member - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT ENTRY - IF true - DO traceln("Reached internal transition error staff member") - ENDRULE - - RULE Increment discharge counter - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT READ DISCHARGE - IF true - DO traceln("Byteman test - discharging") - ENDRULE - - RULE Increment observation counter - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT READ OVERNIGHT_OBSERVATION - IF true - DO traceln("Byteman test - overnight observation") - ENDRULE - """.trimIndent() - - submitBytemanRules(rules) - - val aliceClient = - CordaRPCClient(alice.rpcAddress).start(rpcUser.username, rpcUser.password).proxy - - assertFailsWith { - aliceClient.startFlow(StatemachineErrorHandlingTest::SendAMessageFlow, charlie.nodeInfo.singleIdentity()).returnValue.getOrThrow( - 30.seconds - ) - } - - val output = getBytemanOutput(alice) - - // Check the stdout for the lines generated by byteman - assertEquals(3, output.filter { it.contains("Byteman test - discharging") }.size) - assertEquals(1, output.filter { it.contains("Byteman test - overnight observation") }.size) - val (discharge, observation) = aliceClient.startFlow(StatemachineErrorHandlingTest::GetHospitalCountersFlow).returnValue.get() - assertEquals(3, discharge) - assertEquals(1, observation) - assertEquals(1, aliceClient.stateMachinesSnapshot().size) - // 1 for GetNumberOfCheckpointsFlow - assertEquals(1, aliceClient.startFlow(StatemachineErrorHandlingTest::GetNumberOfCheckpointsFlow).returnValue.get()) - } - } - - /** - * Throws an exception when performing an [Action.CommitTransaction] event after the flow has suspended (has moved to a started state). - * The exception is thrown 5 times. - * - * This causes the transition to be discharged from the hospital 3 times (retries 3 times). On the final retry the transition - * succeeds and the flow finishes. - * - * Each time the flow retries, it begins from the previous checkpoint where it suspended before failing. - * - * 2 of the thrown exceptions are absorbed by the if statement in [TransitionExecutorImpl.executeTransition] that aborts the transition - * if an error transition moves into another error transition. The flow still recovers from this state. 5 exceptions were thrown to - * verify that 3 retries are attempted before recovering. - */ - @Test(timeout=300_000) - fun `error during transition with CommitTransaction action that occurs after the first suspend will retry and complete successfully`() { - startDriver { - val charlie = createNode(CHARLIE_NAME) - val alice = createBytemanNode(ALICE_NAME) - - // seems to be restarting the flow from the beginning every time - val rules = """ - RULE Create Counter - CLASS ${ActionExecutorImpl::class.java.name} - METHOD executeCommitTransaction - AT ENTRY - IF createCounter("counter", $counter) - DO traceln("Counter created") - ENDRULE - - RULE Set flag when executing first suspend - CLASS ${TopLevelTransition::class.java.name} - METHOD suspendTransition - AT ENTRY - IF !flagged("suspend_flag") - DO flag("suspend_flag"); traceln("Setting suspend flag to true") - ENDRULE - - RULE Throw exception on executeCommitTransaction action after first suspend + commit - CLASS ${ActionExecutorImpl::class.java.name} - METHOD executeCommitTransaction - AT ENTRY - IF flagged("suspend_flag") && flagged("commit_flag") && readCounter("counter") < 5 - DO incrementCounter("counter"); traceln("Throwing exception"); throw new java.lang.RuntimeException("die dammit die") - ENDRULE - - RULE Set flag when executing first commit - CLASS ${ActionExecutorImpl::class.java.name} - METHOD executeCommitTransaction - AT ENTRY - IF flagged("suspend_flag") && !flagged("commit_flag") - DO flag("commit_flag"); traceln("Setting commit flag to true") - ENDRULE - - RULE Entering internal error staff member - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT ENTRY - IF true - DO traceln("Reached internal transition error staff member") - ENDRULE - - RULE Increment discharge counter - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT READ DISCHARGE - IF true - DO traceln("Byteman test - discharging") - ENDRULE - - RULE Increment observation counter - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT READ OVERNIGHT_OBSERVATION - IF true - DO traceln("Byteman test - overnight observation") - ENDRULE - """.trimIndent() - - submitBytemanRules(rules) - - val aliceClient = - CordaRPCClient(alice.rpcAddress).start(rpcUser.username, rpcUser.password).proxy - - aliceClient.startFlow(StatemachineErrorHandlingTest::SendAMessageFlow, charlie.nodeInfo.singleIdentity()).returnValue.getOrThrow( - 30.seconds - ) - - val output = getBytemanOutput(alice) - - // Check the stdout for the lines generated by byteman - assertEquals(3, output.filter { it.contains("Byteman test - discharging") }.size) - assertEquals(0, output.filter { it.contains("Byteman test - overnight observation") }.size) - val (discharge, observation) = aliceClient.startFlow(StatemachineErrorHandlingTest::GetHospitalCountersFlow).returnValue.get() - assertEquals(3, discharge) - assertEquals(0, observation) - assertEquals(0, aliceClient.stateMachinesSnapshot().size) - // 1 for GetNumberOfCheckpointsFlow - assertEquals(1, aliceClient.startFlow(StatemachineErrorHandlingTest::GetNumberOfCheckpointsFlow).returnValue.get()) - } - } - - /** - * Throws an exception when performing an [Action.CommitTransaction] event when the flow is finishing. - * The exception is thrown 3 times. - * - * This causes the transition to be discharged from the hospital 3 times (retries 3 times). On the final retry the transition - * succeeds and the flow finishes. - * - * Each time the flow retries, it begins from the previous checkpoint where it suspended before failing. - */ - @Test(timeout=300_000) - fun `error during transition with CommitTransaction action that occurs when completing a flow and deleting its checkpoint will retry and complete successfully`() { - startDriver { - val charlie = createNode(CHARLIE_NAME) - val alice = createBytemanNode(ALICE_NAME) - - // seems to be restarting the flow from the beginning every time - val rules = """ - RULE Create Counter - CLASS ${ActionExecutorImpl::class.java.name} - METHOD executeCommitTransaction - AT ENTRY - IF createCounter("counter", $counter) - DO traceln("Counter created") - ENDRULE - - RULE Set flag when adding action to remove checkpoint - CLASS ${TopLevelTransition::class.java.name} - METHOD flowFinishTransition - AT ENTRY - IF !flagged("remove_checkpoint_flag") - DO flag("remove_checkpoint_flag"); traceln("Setting remove checkpoint flag to true") - ENDRULE - - RULE Throw exception on executeCommitTransaction when removing checkpoint - CLASS ${ActionExecutorImpl::class.java.name} - METHOD executeCommitTransaction - AT ENTRY - IF flagged("remove_checkpoint_flag") && readCounter("counter") < 3 - DO incrementCounter("counter"); clear("remove_checkpoint_flag"); traceln("Throwing exception"); throw new java.lang.RuntimeException("die dammit die") - ENDRULE - - RULE Entering internal error staff member - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT ENTRY - IF true - DO traceln("Reached internal transition error staff member") - ENDRULE - - RULE Increment discharge counter - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT READ DISCHARGE - IF true - DO traceln("Byteman test - discharging") - ENDRULE - - RULE Increment observation counter - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT READ OVERNIGHT_OBSERVATION - IF true - DO traceln("Byteman test - overnight observation") - ENDRULE - """.trimIndent() - - submitBytemanRules(rules) - - val aliceClient = - CordaRPCClient(alice.rpcAddress).start(rpcUser.username, rpcUser.password).proxy - - aliceClient.startFlow(StatemachineErrorHandlingTest::SendAMessageFlow, charlie.nodeInfo.singleIdentity()).returnValue.getOrThrow( - 30.seconds - ) - - val output = getBytemanOutput(alice) - - // Check the stdout for the lines generated by byteman - assertEquals(3, output.filter { it.contains("Byteman test - discharging") }.size) - assertEquals(0, output.filter { it.contains("Byteman test - overnight observation") }.size) - val (discharge, observation) = aliceClient.startFlow(StatemachineErrorHandlingTest::GetHospitalCountersFlow).returnValue.get() - assertEquals(3, discharge) - assertEquals(0, observation) - assertEquals(0, aliceClient.stateMachinesSnapshot().size) - // 1 for GetNumberOfCheckpointsFlow - assertEquals(1, aliceClient.startFlow(StatemachineErrorHandlingTest::GetNumberOfCheckpointsFlow).returnValue.get()) - } - } - - /** - * Throws an exception when replaying a flow that has already successfully created its initial checkpoint. - * - * An exception is thrown when committing a database transaction during a transition to trigger the retry of the flow. Another - * exception is then thrown during the retry itself. - * - * The flow is discharged and replayed from the hospital once. After failing during the replay, the flow is forced into overnight - * observation. It is not ran again after this point - */ - @Test(timeout=300_000) - fun `error during retry of a flow will force the flow into overnight observation`() { - startDriver { - val charlie = createNode(CHARLIE_NAME) - val alice = createBytemanNode(ALICE_NAME) - - val rules = """ - RULE Set flag when executing first suspend - CLASS ${TopLevelTransition::class.java.name} - METHOD suspendTransition - AT ENTRY - IF !flagged("suspend_flag") - DO flag("suspend_flag"); traceln("Setting suspend flag to true") - ENDRULE - - RULE Throw exception on executeCommitTransaction action after first suspend + commit - CLASS ${ActionExecutorImpl::class.java.name} - METHOD executeCommitTransaction - AT ENTRY - IF flagged("suspend_flag") && flagged("commit_flag") && !flagged("commit_exception_flag") - DO flag("commit_exception_flag"); traceln("Throwing exception"); throw new java.lang.RuntimeException("die dammit die") - ENDRULE - - RULE Set flag when executing first commit - CLASS ${ActionExecutorImpl::class.java.name} - METHOD executeCommitTransaction - AT ENTRY - IF flagged("suspend_flag") && !flagged("commit_flag") - DO flag("commit_flag"); traceln("Setting commit flag to true") - ENDRULE - - RULE Throw exception on retry - CLASS ${SingleThreadedStateMachineManager::class.java.name} - METHOD addAndStartFlow - AT ENTRY - IF flagged("suspend_flag") && flagged("commit_flag") && !flagged("retry_exception_flag") - DO flag("retry_exception_flag"); traceln("Throwing retry exception"); throw new java.lang.RuntimeException("Here we go again") - ENDRULE - - RULE Entering internal error staff member - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT ENTRY - IF true - DO traceln("Reached internal transition error staff member") - ENDRULE - - RULE Increment discharge counter - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT READ DISCHARGE - IF true - DO traceln("Byteman test - discharging") - ENDRULE - - RULE Increment observation counter - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT READ OVERNIGHT_OBSERVATION - IF true - DO traceln("Byteman test - overnight observation") - ENDRULE - """.trimIndent() - - submitBytemanRules(rules) - - val aliceClient = - CordaRPCClient(alice.rpcAddress).start(rpcUser.username, rpcUser.password).proxy - - assertFailsWith { - aliceClient.startFlow(StatemachineErrorHandlingTest::SendAMessageFlow, charlie.nodeInfo.singleIdentity()).returnValue.getOrThrow( - 30.seconds - ) - } - - val output = getBytemanOutput(alice) - - // Check the stdout for the lines generated by byteman - assertEquals(1, output.filter { it.contains("Byteman test - discharging") }.size) - assertEquals(0, output.filter { it.contains("Byteman test - overnight observation") }.size) - val (discharge, observation) = aliceClient.startFlow(StatemachineErrorHandlingTest::GetHospitalCountersFlow).returnValue.get() - assertEquals(1, discharge) - assertEquals(1, observation) - assertEquals(1, aliceClient.stateMachinesSnapshot().size) - // 1 for the errored flow kept for observation and another for GetNumberOfCheckpointsFlow - assertEquals(2, aliceClient.startFlow(StatemachineErrorHandlingTest::GetNumberOfCheckpointsFlow).returnValue.get()) - } - } - - /** - * Throws an exception when replaying a flow that has already successfully created its initial checkpoint. - * - * An exception is thrown when committing a database transaction during a transition to trigger the retry of the flow. Another - * exception is then thrown during the database commit that comes as part of retrying a flow. - * - * The flow is discharged and replayed from the hospital once. When the database commit failure occurs as part of retrying the - * flow, the starting and completion of the retried flow is affected. In other words, the error occurs as part of the replay, but the - * flow will still finish successfully. This is due to the even being scheduled as part of the retry and the failure in the database - * commit occurs after this point. As the flow is already scheduled, the failure has not affect on it. - */ - @Test(timeout=300_000) - fun `error during commit transaction action when retrying a flow will retry the flow again and complete successfully`() { - startDriver { - val charlie = createNode(CHARLIE_NAME) - val alice = createBytemanNode(ALICE_NAME) - - val rules = """ - RULE Set flag when executing first suspend - CLASS ${TopLevelTransition::class.java.name} - METHOD suspendTransition - AT ENTRY - IF !flagged("suspend_flag") - DO flag("suspend_flag"); traceln("Setting suspend flag to true") - ENDRULE - - RULE Throw exception on executeCommitTransaction action after first suspend + commit - CLASS ${ActionExecutorImpl::class.java.name} - METHOD executeCommitTransaction - AT ENTRY - IF flagged("suspend_flag") && flagged("commit_flag") && !flagged("commit_exception_flag") - DO flag("commit_exception_flag"); traceln("Throwing exception"); throw new java.lang.RuntimeException("die dammit die") - ENDRULE - - RULE Set flag when executing first commit - CLASS ${ActionExecutorImpl::class.java.name} - METHOD executeCommitTransaction - AT ENTRY - IF flagged("suspend_flag") && !flagged("commit_flag") - DO flag("commit_flag"); traceln("Setting commit flag to true") - ENDRULE - - RULE Throw exception on retry - CLASS ${ActionExecutorImpl::class.java.name} - METHOD executeCommitTransaction - AT ENTRY - IF flagged("suspend_flag") && flagged("commit_exception_flag") && !flagged("retry_exception_flag") - DO flag("retry_exception_flag"); traceln("Throwing retry exception"); throw new java.lang.RuntimeException("Here we go again") - ENDRULE - - RULE Entering internal error staff member - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT ENTRY - IF true - DO traceln("Reached internal transition error staff member") - ENDRULE - - RULE Increment discharge counter - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT READ DISCHARGE - IF true - DO traceln("Byteman test - discharging") - ENDRULE - - RULE Increment observation counter - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT READ OVERNIGHT_OBSERVATION - IF true - DO traceln("Byteman test - overnight observation") - ENDRULE - """.trimIndent() - - submitBytemanRules(rules) - - val aliceClient = - CordaRPCClient(alice.rpcAddress).start(rpcUser.username, rpcUser.password).proxy - - aliceClient.startFlow(StatemachineErrorHandlingTest::SendAMessageFlow, charlie.nodeInfo.singleIdentity()).returnValue.getOrThrow( - 30.seconds - ) - - val output = getBytemanOutput(alice) - - // Check the stdout for the lines generated by byteman - assertEquals(1, output.filter { it.contains("Byteman test - discharging") }.size) - assertEquals(0, output.filter { it.contains("Byteman test - overnight observation") }.size) - val (discharge, observation) = aliceClient.startFlow(StatemachineErrorHandlingTest::GetHospitalCountersFlow).returnValue.get() - assertEquals(1, discharge) - assertEquals(0, observation) - assertEquals(0, aliceClient.stateMachinesSnapshot().size) - // 1 for GetNumberOfCheckpointsFlow - assertEquals(1, aliceClient.startFlow(StatemachineErrorHandlingTest::GetNumberOfCheckpointsFlow).returnValue.get()) - } - } - - /** - * Throws an exception when replaying a flow that has not made its initial checkpoint. - * - * An exception is thrown when committing a database transaction during a transition to trigger the retry of the flow. Another - * exception is then thrown during the retry itself. - * - * The flow is discharged and replayed from the hospital once. After failing during the replay, the flow is forced into overnight - * observation. It is not ran again after this point - * - * CORDA-3352 - it is currently hanging after putting the flow in for observation - * - */ - @Test(timeout=300_000) -@Ignore - fun `error during retrying a flow that failed when committing its original checkpoint will force the flow into overnight observation`() { - startDriver { - val charlie = createNode(CHARLIE_NAME) - val alice = createBytemanNode(ALICE_NAME) - - val rules = """ - RULE Throw exception on executeCommitTransaction action after first suspend + commit - CLASS ${ActionExecutorImpl::class.java.name} - METHOD executeCommitTransaction - AT ENTRY - IF !flagged("commit_exception_flag") - DO flag("commit_exception_flag"); traceln("Throwing exception"); throw new java.lang.RuntimeException("die dammit die") - ENDRULE - - RULE Throw exception on retry - CLASS ${SingleThreadedStateMachineManager::class.java.name} - METHOD onExternalStartFlow - AT ENTRY - IF flagged("commit_exception_flag") && !flagged("retry_exception_flag") - DO flag("retry_exception_flag"); traceln("Throwing retry exception"); throw new java.lang.RuntimeException("Here we go again") - ENDRULE - - RULE Entering internal error staff member - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT ENTRY - IF true - DO traceln("Reached internal transition error staff member") - ENDRULE - - RULE Increment discharge counter - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT READ DISCHARGE - IF true - DO traceln("Byteman test - discharging") - ENDRULE - - RULE Increment observation counter - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT READ OVERNIGHT_OBSERVATION - IF true - DO traceln("Byteman test - overnight observation") - ENDRULE - """.trimIndent() - - submitBytemanRules(rules) - - val aliceClient = - CordaRPCClient(alice.rpcAddress).start(rpcUser.username, rpcUser.password).proxy - - assertFailsWith { - aliceClient.startFlow(StatemachineErrorHandlingTest::SendAMessageFlow, charlie.nodeInfo.singleIdentity()).returnValue.getOrThrow( - 30.seconds - ) - } - - val output = getBytemanOutput(alice) - - // Check the stdout for the lines generated by byteman - assertEquals(1, output.filter { it.contains("Byteman test - discharging") }.size) - assertEquals(0, output.filter { it.contains("Byteman test - overnight observation") }.size) - val (discharge, observation) = aliceClient.startFlow(StatemachineErrorHandlingTest::GetHospitalCountersFlow).returnValue.get() - assertEquals(1, discharge) - assertEquals(1, observation) - assertEquals(1, aliceClient.stateMachinesSnapshot().size) - // 1 for the errored flow kept for observation and another for GetNumberOfCheckpointsFlow - assertEquals(2, aliceClient.startFlow(StatemachineErrorHandlingTest::GetNumberOfCheckpointsFlow).returnValue.get()) - } - } - - /** - * Throws a [ConstraintViolationException] when performing an [Action.CommitTransaction] event when the flow is finishing. - * The exception is thrown 4 times. - * - * This causes the transition to be discharged from the hospital 3 times (retries 3 times) and then be kept in for observation. - * - * Each time the flow retries, it begins from the previous checkpoint where it suspended before failing. - */ - @Test(timeout=300_000) - fun `error during transition with CommitTransaction action and ConstraintViolationException that occurs when completing a flow will retry and be kept for observation if error persists`() { - startDriver { - val charlie = createNode(CHARLIE_NAME) - val alice = createBytemanNode(ALICE_NAME) - - val rules = """ - RULE Create Counter - CLASS ${ActionExecutorImpl::class.java.name} - METHOD executeCommitTransaction - AT ENTRY - IF createCounter("counter", $counter) - DO traceln("Counter created") - ENDRULE - - RULE Set flag when adding action to remove checkpoint - CLASS ${TopLevelTransition::class.java.name} - METHOD flowFinishTransition - AT ENTRY - IF !flagged("remove_checkpoint_flag") - DO flag("remove_checkpoint_flag"); traceln("Setting remove checkpoint flag to true") - ENDRULE - - RULE Throw exception on executeCommitTransaction when removing checkpoint - CLASS ${ActionExecutorImpl::class.java.name} - METHOD executeCommitTransaction - AT ENTRY - IF flagged("remove_checkpoint_flag") && readCounter("counter") < 4 - DO incrementCounter("counter"); - clear("remove_checkpoint_flag"); - traceln("Throwing exception"); - throw new org.hibernate.exception.ConstraintViolationException("This flow has a terminal condition", new java.sql.SQLException(), "made up constraint") - ENDRULE - - RULE Entering duplicate insert staff member - CLASS ${StaffedFlowHospital.DuplicateInsertSpecialist::class.java.name} - METHOD consult - AT ENTRY - IF true - DO traceln("Reached duplicate insert staff member") - ENDRULE - - RULE Increment discharge counter - CLASS ${StaffedFlowHospital.DuplicateInsertSpecialist::class.java.name} - METHOD consult - AT READ DISCHARGE - IF true - DO traceln("Byteman test - discharging") - ENDRULE - - RULE Increment not my speciality counter - CLASS ${StaffedFlowHospital.DuplicateInsertSpecialist::class.java.name} - METHOD consult - AT READ NOT_MY_SPECIALTY - IF true - DO traceln("Byteman test - not my speciality") - ENDRULE - """.trimIndent() - - submitBytemanRules(rules) - - val aliceClient = - CordaRPCClient(alice.rpcAddress).start(rpcUser.username, rpcUser.password).proxy - - assertFailsWith { - aliceClient.startFlow(StatemachineErrorHandlingTest::SendAMessageFlow, charlie.nodeInfo.singleIdentity()).returnValue.getOrThrow( - 30.seconds - ) - } - - val output = getBytemanOutput(alice) - - // Check the stdout for the lines generated by byteman - assertEquals(3, output.filter { it.contains("Byteman test - discharging") }.size) - assertEquals(1, output.filter { it.contains("Byteman test - not my speciality") }.size) - val (discharge, observation) = aliceClient.startFlow(StatemachineErrorHandlingTest::GetHospitalCountersFlow).returnValue.get() - assertEquals(3, discharge) - assertEquals(1, observation) - assertEquals(1, aliceClient.stateMachinesSnapshot().size) - // 1 for errored flow and 1 for GetNumberOfCheckpointsFlow - assertEquals(2, aliceClient.startFlow(StatemachineErrorHandlingTest::GetNumberOfCheckpointsFlow).returnValue.get()) - } - } - - /** - * Throws an exception when performing an [Action.CommitTransaction] event on a responding flow. The failure prevents the node from saving - * its original checkpoint. - * - * The exception is thrown 5 times. - * - * This causes the transition to be discharged from the hospital 3 times (retries 3 times). On the final retry the transition - * succeeds and the flow finishes. - * - * Each time the flow retries, it starts from the beginning of the flow (due to being in an unstarted state). - * - * 2 of the thrown exceptions are absorbed by the if statement in [TransitionExecutorImpl.executeTransition] that aborts the transition - * if an error transition moves into another error transition. The flow still recovers from this state. 5 exceptions were thrown to verify - * that 3 retries are attempted before recovering. - */ - @Test(timeout=300_000) - fun `responding flow - error during transition with CommitTransaction action that occurs during the beginning of execution will retry and complete successfully`() { - startDriver { - val charlie = createBytemanNode(CHARLIE_NAME) - val alice = createNode(ALICE_NAME) - - val rules = """ - RULE Create Counter - CLASS ${ActionExecutorImpl::class.java.name} - METHOD executeCommitTransaction - AT ENTRY - IF createCounter("counter", $counter) - DO traceln("Counter created") - ENDRULE - - RULE Throw exception on executeCommitTransaction action - CLASS ${ActionExecutorImpl::class.java.name} - METHOD executeCommitTransaction - AT ENTRY - IF readCounter("counter") < 5 - DO incrementCounter("counter"); traceln("Throwing exception"); throw new java.lang.RuntimeException("die dammit die") - ENDRULE - - RULE Entering internal error staff member - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT ENTRY - IF true - DO traceln("Reached internal transition error staff member") - ENDRULE - - RULE Increment discharge counter - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT READ DISCHARGE - IF true - DO traceln("Byteman test - discharging") - ENDRULE - - RULE Increment observation counter - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT READ OVERNIGHT_OBSERVATION - IF true - DO traceln("Byteman test - overnight observation") - ENDRULE - """.trimIndent() - - submitBytemanRules(rules) - - val aliceClient = - CordaRPCClient(alice.rpcAddress).start(rpcUser.username, rpcUser.password).proxy - val charlieClient = - CordaRPCClient(charlie.rpcAddress).start(rpcUser.username, rpcUser.password).proxy - - aliceClient.startFlow(StatemachineErrorHandlingTest::SendAMessageFlow, charlie.nodeInfo.singleIdentity()).returnValue.getOrThrow( - 30.seconds - ) - - val output = getBytemanOutput(charlie) - - // Check the stdout for the lines generated by byteman - assertEquals(3, output.filter { it.contains("Byteman test - discharging") }.size) - assertEquals(0, output.filter { it.contains("Byteman test - overnight observation") }.size) - val (discharge, observation) = charlieClient.startFlow(StatemachineErrorHandlingTest::GetHospitalCountersFlow).returnValue.get() - assertEquals(3, discharge) - assertEquals(0, observation) - assertEquals(0, aliceClient.stateMachinesSnapshot().size) - assertEquals(0, charlieClient.stateMachinesSnapshot().size) - // 1 for GetNumberOfCheckpointsFlow - assertEquals(1, aliceClient.startFlow(StatemachineErrorHandlingTest::GetNumberOfCheckpointsFlow).returnValue.get()) - // 1 for GetNumberOfCheckpointsFlow - assertEquals(1, charlieClient.startFlow(StatemachineErrorHandlingTest::GetNumberOfCheckpointsFlow).returnValue.get()) - } - } - - /** - * Throws an exception when performing an [Action.CommitTransaction] event on a responding flow. The failure prevents the node from saving - * its original checkpoint. - * - * The exception is thrown 5 times. - * - * This causes the transition to be discharged from the hospital 3 times (retries 3 times) and then be kept in for observation. - * - * Each time the flow retries, it starts from the beginning of the flow (due to being in an unstarted state). - * - * 2 of the thrown exceptions are absorbed by the if statement in [TransitionExecutorImpl.executeTransition] that aborts the transition - * if an error transition moves into another error transition. The flow still recovers from this state. 5 exceptions were thrown to verify - * that 3 retries are attempted before recovering. - * - * The final asserts for checking the checkpoints on the nodes are correct since the responding node can replay the flow starting events - * from artemis. Therefore, the checkpoint is missing due the failures from saving the original checkpoint. But, the node will still be - * able to recover when the node is restarted (by using the events). The initiating flow maintains the checkpoint as it is waiting for - * the responding flow to recover and finish its flow. - */ - @Test(timeout=300_000) - fun `responding flow - error during transition with CommitTransaction action that occurs during the beginning of execution will retry and be kept for observation if error persists`() { - startDriver { - val charlie = createBytemanNode(CHARLIE_NAME) - val alice = createNode(ALICE_NAME) - - val rules = """ - RULE Create Counter - CLASS ${ActionExecutorImpl::class.java.name} - METHOD executeCommitTransaction - AT ENTRY - IF createCounter("counter", $counter) - DO traceln("Counter created") - ENDRULE - - RULE Throw exception on executeCommitTransaction action - CLASS ${ActionExecutorImpl::class.java.name} - METHOD executeCommitTransaction - AT ENTRY - IF readCounter("counter") < 7 - DO incrementCounter("counter"); traceln("Throwing exception"); throw new java.lang.RuntimeException("die dammit die") - ENDRULE - - RULE Entering internal error staff member - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT ENTRY - IF true - DO traceln("Reached internal transition error staff member") - ENDRULE - - RULE Increment discharge counter - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT READ DISCHARGE - IF true - DO traceln("Byteman test - discharging") - ENDRULE - - RULE Increment observation counter - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT READ OVERNIGHT_OBSERVATION - IF true - DO traceln("Byteman test - overnight observation") - ENDRULE - """.trimIndent() - - submitBytemanRules(rules) - - val aliceClient = - CordaRPCClient(alice.rpcAddress).start(rpcUser.username, rpcUser.password).proxy - val charlieClient = - CordaRPCClient(charlie.rpcAddress).start(rpcUser.username, rpcUser.password).proxy - - assertFailsWith { - aliceClient.startFlow(StatemachineErrorHandlingTest::SendAMessageFlow, charlie.nodeInfo.singleIdentity()).returnValue.getOrThrow( - 30.seconds - ) - } - - val output = getBytemanOutput(charlie) - - // Check the stdout for the lines generated by byteman - assertEquals(3, output.filter { it.contains("Byteman test - discharging") }.size) - assertEquals(1, output.filter { it.contains("Byteman test - overnight observation") }.size) - val (discharge, observation) = charlieClient.startFlow(StatemachineErrorHandlingTest::GetHospitalCountersFlow).returnValue.get() - assertEquals(3, discharge) - assertEquals(1, observation) - assertEquals(1, aliceClient.stateMachinesSnapshot().size) - assertEquals(1, charlieClient.stateMachinesSnapshot().size) - // 1 for the flow that is waiting for the errored counterparty flow to finish and 1 for GetNumberOfCheckpointsFlow - assertEquals(2, aliceClient.startFlow(StatemachineErrorHandlingTest::GetNumberOfCheckpointsFlow).returnValue.get()) - // 1 for GetNumberOfCheckpointsFlow - // the checkpoint is not persisted since it kept failing the original checkpoint commit - // the flow will recover since artemis will keep the events and replay them on node restart - assertEquals(1, charlieClient.startFlow(StatemachineErrorHandlingTest::GetNumberOfCheckpointsFlow).returnValue.get()) - } - } - - /** - * Throws an exception when performing an [Action.CommitTransaction] event when the flow is finishing on a responding node. - * - * The exception is thrown 3 times. - * - * This causes the transition to be discharged from the hospital 3 times (retries 3 times). On the final retry the transition - * succeeds and the flow finishes. - */ - @Test(timeout=300_000) - fun `responding flow - error during transition with CommitTransaction action that occurs when completing a flow and deleting its checkpoint will retry and complete successfully`() { - startDriver { - val charlie = createBytemanNode(CHARLIE_NAME) - val alice = createNode(ALICE_NAME) - - val rules = """ - RULE Create Counter - CLASS ${ActionExecutorImpl::class.java.name} - METHOD executeCommitTransaction - AT ENTRY - IF createCounter("counter", $counter) - DO traceln("Counter created") - ENDRULE - - RULE Set flag when adding action to remove checkpoint - CLASS ${TopLevelTransition::class.java.name} - METHOD flowFinishTransition - AT ENTRY - IF !flagged("remove_checkpoint_flag") - DO flag("remove_checkpoint_flag"); traceln("Setting remove checkpoint flag to true") - ENDRULE - - RULE Throw exception on executeCommitTransaction when removing checkpoint - CLASS ${ActionExecutorImpl::class.java.name} - METHOD executeCommitTransaction - AT ENTRY - IF flagged("remove_checkpoint_flag") && readCounter("counter") < 3 - DO incrementCounter("counter"); - clear("remove_checkpoint_flag"); - traceln("Throwing exception"); - throw new java.lang.RuntimeException("die dammit die") - ENDRULE - - RULE Entering internal error staff member - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT ENTRY - IF true - DO traceln("Reached internal transition error staff member") - ENDRULE - - RULE Increment discharge counter - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT READ DISCHARGE - IF true - DO traceln("Byteman test - discharging") - ENDRULE - - RULE Increment observation counter - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT READ OVERNIGHT_OBSERVATION - IF true - DO traceln("Byteman test - overnight observation") - ENDRULE - """.trimIndent() - - submitBytemanRules(rules) - - val aliceClient = - CordaRPCClient(alice.rpcAddress).start(rpcUser.username, rpcUser.password).proxy - val charlieClient = - CordaRPCClient(charlie.rpcAddress).start(rpcUser.username, rpcUser.password).proxy - - aliceClient.startFlow(StatemachineErrorHandlingTest::SendAMessageFlow, charlie.nodeInfo.singleIdentity()).returnValue.getOrThrow( - 30.seconds - ) - - val output = getBytemanOutput(charlie) - - // Check the stdout for the lines generated by byteman - assertEquals(3, output.filter { it.contains("Byteman test - discharging") }.size) - assertEquals(0, output.filter { it.contains("Byteman test - overnight observation") }.size) - val (discharge, observation) = charlieClient.startFlow(StatemachineErrorHandlingTest::GetHospitalCountersFlow).returnValue.get() - assertEquals(3, discharge) - assertEquals(0, observation) - assertEquals(0, aliceClient.stateMachinesSnapshot().size) - assertEquals(0, charlieClient.stateMachinesSnapshot().size) - // 1 for GetNumberOfCheckpointsFlow - assertEquals(1, aliceClient.startFlow(StatemachineErrorHandlingTest::GetNumberOfCheckpointsFlow).returnValue.get()) - // 1 for GetNumberOfCheckpointsFlow - assertEquals(1, charlieClient.startFlow(StatemachineErrorHandlingTest::GetNumberOfCheckpointsFlow).returnValue.get()) - } - } -} \ No newline at end of file diff --git a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineKillFlowErrorHandlingTest.kt b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineKillFlowErrorHandlingTest.kt deleted file mode 100644 index 6e4f7bf2d8..0000000000 --- a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineKillFlowErrorHandlingTest.kt +++ /dev/null @@ -1,322 +0,0 @@ -package net.corda.node.services.statemachine - -import co.paralleluniverse.fibers.Suspendable -import net.corda.client.rpc.CordaRPCClient -import net.corda.core.flows.FlowLogic -import net.corda.core.flows.StartableByRPC -import net.corda.core.messaging.startFlow -import net.corda.core.messaging.startTrackedFlow -import net.corda.core.utilities.ProgressTracker -import net.corda.core.utilities.getOrThrow -import net.corda.core.utilities.seconds -import net.corda.testing.core.ALICE_NAME -import net.corda.testing.core.CHARLIE_NAME -import net.corda.testing.core.singleIdentity -import org.junit.Test -import java.time.Duration -import java.time.temporal.ChronoUnit -import java.util.concurrent.TimeoutException -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith -import kotlin.test.assertTrue - -@Suppress("MaxLineLength") // Byteman rules cannot be easily wrapped -class StatemachineKillFlowErrorHandlingTest : StatemachineErrorHandlingTest() { - - /** - * Triggers `killFlow` while the flow is suspended causing a [InterruptedException] to be thrown and passed through the hospital. - * - * The flow terminates and is not retried. - * - * No pass through the hospital is recorded. As the flow is marked as `isRemoved`. - */ - @Test(timeout=300_000) - fun `error during transition due to an InterruptedException (killFlow) will terminate the flow`() { - startDriver { - val alice = createBytemanNode(ALICE_NAME) - - val rules = """ - RULE Entering internal error staff member - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT ENTRY - IF true - DO traceln("Reached internal transition error staff member") - ENDRULE - - RULE Increment discharge counter - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT READ DISCHARGE - IF true - DO traceln("Byteman test - discharging") - ENDRULE - - RULE Increment observation counter - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT READ OVERNIGHT_OBSERVATION - IF true - DO traceln("Byteman test - overnight observation") - ENDRULE - - RULE Increment terminal counter - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT READ TERMINAL - IF true - DO traceln("Byteman test - terminal") - ENDRULE - """.trimIndent() - - submitBytemanRules(rules) - - val aliceClient = - CordaRPCClient(alice.rpcAddress).start(rpcUser.username, rpcUser.password).proxy - - val flow = aliceClient.startTrackedFlow(StatemachineKillFlowErrorHandlingTest::SleepFlow) - - var flowKilled = false - flow.progress.subscribe { - if (it == SleepFlow.STARTED.label) { - Thread.sleep(5000) - flowKilled = aliceClient.killFlow(flow.id) - } - } - - assertFailsWith { flow.returnValue.getOrThrow(20.seconds) } - - val output = getBytemanOutput(alice) - - assertTrue(flowKilled) - // Check the stdout for the lines generated by byteman - assertEquals(0, output.filter { it.contains("Byteman test - discharging") }.size) - assertEquals(0, output.filter { it.contains("Byteman test - overnight observation") }.size) - val numberOfTerminalDiagnoses = output.filter { it.contains("Byteman test - terminal") }.size - assertEquals(1, numberOfTerminalDiagnoses) - val (discharge, observation) = aliceClient.startFlow(StatemachineErrorHandlingTest::GetHospitalCountersFlow).returnValue.get() - assertEquals(0, discharge) - assertEquals(0, observation) - assertEquals(0, aliceClient.stateMachinesSnapshot().size) - // 1 for GetNumberOfCheckpointsFlow - assertEquals(1, aliceClient.startFlow(StatemachineErrorHandlingTest::GetNumberOfCheckpointsFlow).returnValue.get()) - } - } - - /** - * Triggers `killFlow` during user application code. - * - * The user application code is mimicked by a [Thread.sleep] which is importantly not placed inside the [Suspendable] - * call function. Placing it inside a [Suspendable] function causes quasar to behave unexpectedly. - * - * Although the call to kill the flow is made during user application code. It will not be removed / stop processing - * until the next suspension point is reached within the flow. - * - * The flow terminates and is not retried. - * - * No pass through the hospital is recorded. As the flow is marked as `isRemoved`. - */ - @Test(timeout=300_000) - fun `flow killed during user code execution stops and removes the flow correctly`() { - startDriver { - val alice = createBytemanNode(ALICE_NAME) - - val rules = """ - RULE Entering internal error staff member - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT ENTRY - IF true - DO traceln("Reached internal transition error staff member") - ENDRULE - - RULE Increment discharge counter - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT READ DISCHARGE - IF true - DO traceln("Byteman test - discharging") - ENDRULE - - RULE Increment observation counter - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT READ OVERNIGHT_OBSERVATION - IF true - DO traceln("Byteman test - overnight observation") - ENDRULE - - RULE Increment terminal counter - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT READ TERMINAL - IF true - DO traceln("Byteman test - terminal") - ENDRULE - """.trimIndent() - - submitBytemanRules(rules) - - val aliceClient = - CordaRPCClient(alice.rpcAddress).start(rpcUser.username, rpcUser.password).proxy - - val flow = aliceClient.startTrackedFlow(StatemachineKillFlowErrorHandlingTest::ThreadSleepFlow) - - var flowKilled = false - flow.progress.subscribe { - if (it == ThreadSleepFlow.STARTED.label) { - Thread.sleep(5000) - flowKilled = aliceClient.killFlow(flow.id) - } - } - - assertFailsWith { flow.returnValue.getOrThrow(30.seconds) } - - val output = getBytemanOutput(alice) - - assertTrue(flowKilled) - // Check the stdout for the lines generated by byteman - assertEquals(0, output.filter { it.contains("Byteman test - discharging") }.size) - assertEquals(0, output.filter { it.contains("Byteman test - overnight observation") }.size) - val numberOfTerminalDiagnoses = output.filter { it.contains("Byteman test - terminal") }.size - println(numberOfTerminalDiagnoses) - assertEquals(0, numberOfTerminalDiagnoses) - val (discharge, observation) = aliceClient.startFlow(StatemachineErrorHandlingTest::GetHospitalCountersFlow).returnValue.get() - assertEquals(0, discharge) - assertEquals(0, observation) - assertEquals(0, aliceClient.stateMachinesSnapshot().size) - // 1 for GetNumberOfCheckpointsFlow - assertEquals(1, aliceClient.startFlow(StatemachineErrorHandlingTest::GetNumberOfCheckpointsFlow).returnValue.get()) - } - } - - /** - * Triggers `killFlow` after the flow has already been sent to observation. The flow is not running at this point and - * all that remains is its checkpoint in the database. - * - * The flow terminates and is not retried. - * - * Killing the flow does not lead to any passes through the hospital. All the recorded passes through the hospital are - * from the original flow that was put in for observation. - */ - @Test(timeout=300_000) - fun `flow killed when it is in the flow hospital for observation is removed correctly`() { - startDriver { - val alice = createBytemanNode(ALICE_NAME) - val charlie = createNode(CHARLIE_NAME) - - val rules = """ - RULE Create Counter - CLASS ${ActionExecutorImpl::class.java.name} - METHOD executeSendInitial - AT ENTRY - IF createCounter("counter", $counter) - DO traceln("Counter created") - ENDRULE - - RULE Throw exception on executeSendInitial action - CLASS ${ActionExecutorImpl::class.java.name} - METHOD executeSendInitial - AT ENTRY - IF readCounter("counter") < 4 - DO incrementCounter("counter"); traceln("Throwing exception"); throw new java.lang.RuntimeException("die dammit die") - ENDRULE - - RULE Entering internal error staff member - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT ENTRY - IF true - DO traceln("Reached internal transition error staff member") - ENDRULE - - RULE Increment discharge counter - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT READ DISCHARGE - IF true - DO traceln("Byteman test - discharging") - ENDRULE - - RULE Increment observation counter - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT READ OVERNIGHT_OBSERVATION - IF true - DO traceln("Byteman test - overnight observation") - ENDRULE - - RULE Increment terminal counter - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT READ TERMINAL - IF true - DO traceln("Byteman test - terminal") - ENDRULE - """.trimIndent() - - submitBytemanRules(rules) - - val aliceClient = - CordaRPCClient(alice.rpcAddress).start(rpcUser.username, rpcUser.password).proxy - - val flow = aliceClient.startFlow(StatemachineErrorHandlingTest::SendAMessageFlow, charlie.nodeInfo.singleIdentity()) - - assertFailsWith { flow.returnValue.getOrThrow(20.seconds) } - - aliceClient.killFlow(flow.id) - - val output = getBytemanOutput(alice) - - // Check the stdout for the lines generated by byteman - assertEquals(3, output.filter { it.contains("Byteman test - discharging") }.size) - assertEquals(1, output.filter { it.contains("Byteman test - overnight observation") }.size) - val numberOfTerminalDiagnoses = output.filter { it.contains("Byteman test - terminal") }.size - assertEquals(0, numberOfTerminalDiagnoses) - val (discharge, observation) = aliceClient.startFlow(StatemachineErrorHandlingTest::GetHospitalCountersFlow).returnValue.get() - assertEquals(3, discharge) - assertEquals(1, observation) - assertEquals(0, aliceClient.stateMachinesSnapshot().size) - // 1 for GetNumberOfCheckpointsFlow - assertEquals(1, aliceClient.startFlow(StatemachineErrorHandlingTest::GetNumberOfCheckpointsFlow).returnValue.get()) - } - } - - @StartableByRPC - class SleepFlow : FlowLogic() { - - object STARTED : ProgressTracker.Step("I am ready to die") - - override val progressTracker = ProgressTracker(STARTED) - - @Suspendable - override fun call() { - sleep(Duration.of(1, ChronoUnit.SECONDS)) - progressTracker.currentStep = STARTED - sleep(Duration.of(2, ChronoUnit.MINUTES)) - } - } - - @StartableByRPC - class ThreadSleepFlow : FlowLogic() { - - object STARTED : ProgressTracker.Step("I am ready to die") - - override val progressTracker = ProgressTracker(STARTED) - - @Suspendable - override fun call() { - sleep(Duration.of(1, ChronoUnit.SECONDS)) - progressTracker.currentStep = STARTED - logger.info("Starting ${ThreadSleepFlow::class.qualifiedName} application sleep") - sleep() - logger.info("Finished ${ThreadSleepFlow::class.qualifiedName} application sleep") - sleep(Duration.of(2, ChronoUnit.MINUTES)) - } - - // Sleep is moved outside of `@Suspendable` function to prevent issues with Quasar - private fun sleep() { - Thread.sleep(20000) - } - } -} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/ActionExecutor.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/ActionExecutor.kt index 7c2bd77fd8..8e4fb07582 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/ActionExecutor.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/ActionExecutor.kt @@ -1,6 +1,7 @@ package net.corda.node.services.statemachine import co.paralleluniverse.fibers.Suspendable +import java.sql.SQLException /** * An executor of a single [Action]. @@ -10,5 +11,6 @@ interface ActionExecutor { * Execute [action] by [fiber]. */ @Suspendable + @Throws(SQLException::class) fun executeAction(fiber: FlowFiber, action: Action) } diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/ActionExecutorImpl.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/ActionExecutorImpl.kt index f96542a922..08cac92f90 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/ActionExecutorImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/ActionExecutorImpl.kt @@ -19,6 +19,7 @@ import net.corda.node.services.api.ServiceHubInternal import net.corda.nodeapi.internal.persistence.contextDatabase import net.corda.nodeapi.internal.persistence.contextTransaction import net.corda.nodeapi.internal.persistence.contextTransactionOrNull +import java.sql.SQLException import java.time.Duration import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicLong @@ -212,6 +213,7 @@ class ActionExecutorImpl( } @Suspendable + @Throws(SQLException::class) private fun executeCreateTransaction() { if (contextTransactionOrNull != null) { throw IllegalStateException("Refusing to create a second transaction") @@ -225,6 +227,7 @@ class ActionExecutorImpl( } @Suspendable + @Throws(SQLException::class) private fun executeCommitTransaction() { try { contextTransaction.commit() diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/Event.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/Event.kt index a7d15c87c8..32321088e2 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/Event.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/Event.kt @@ -40,7 +40,7 @@ sealed class Event { * Signal that an error has happened. This may be due to an uncaught exception in the flow or some external error. * @param exception the exception itself. */ - data class Error(val exception: Throwable) : Event() + data class Error(val exception: Throwable, val rollback: Boolean = true) : Event() /** * Signal that a ledger transaction has committed. This is an event completing a [FlowIORequest.WaitForLedgerCommit] diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt index 4a9a407473..7771cac91f 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt @@ -258,12 +258,15 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, openThreadLocalWormhole() setLoggingContext() - initialiseFlow() logger.debug { "Calling flow: $logic" } val startTime = System.nanoTime() + var initialised = false val resultOrError = try { + initialiseFlow() + initialised = true + // This sets the Cordapp classloader on the contextClassLoader of the current thread. // Needed because in previous versions of the finance app we used Thread.contextClassLoader to resolve services defined in cordapps. Thread.currentThread().contextClassLoader = (serviceHub.cordappProvider as CordappProviderImpl).cordappLoader.appClassLoader @@ -284,14 +287,14 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, Event.FlowFinish(resultOrError.value, softLocksId) } is Try.Failure -> { - Event.Error(resultOrError.exception) + Event.Error(resultOrError.exception, initialised) } } // Immediately process the last event. This is to make sure the transition can assume that it has an open // database transaction. val continuation = processEventImmediately( finalEvent, - isDbTransactionOpenOnEntry = true, + isDbTransactionOpenOnEntry = initialised, isDbTransactionOpenOnExit = false ) if (continuation == FlowContinuation.ProcessEvents) { @@ -309,8 +312,8 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, @Suspendable private fun initialiseFlow() { processEventsUntilFlowIsResumed( - isDbTransactionOpenOnEntry = false, - isDbTransactionOpenOnExit = true + isDbTransactionOpenOnEntry = false, + isDbTransactionOpenOnExit = true ) } diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/SingleThreadedStateMachineManager.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/SingleThreadedStateMachineManager.kt index cf6b707bd6..00b9d0149c 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/SingleThreadedStateMachineManager.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/SingleThreadedStateMachineManager.kt @@ -354,67 +354,55 @@ class SingleThreadedStateMachineManager( override fun retryFlowFromSafePoint(currentState: StateMachineState) { // Get set of external events val flowId = currentState.flowLogic.runId - try { - val oldFlowLeftOver = mutex.locked { flows[flowId] }?.fiber?.transientValues?.value?.eventQueue - if (oldFlowLeftOver == null) { - logger.error("Unable to find flow for flow $flowId. Something is very wrong. The flow will not retry.") + val oldFlowLeftOver = mutex.locked { flows[flowId] }?.fiber?.transientValues?.value?.eventQueue + if (oldFlowLeftOver == null) { + logger.error("Unable to find flow for flow $flowId. Something is very wrong. The flow will not retry.") + return + } + val flow = if (currentState.isAnyCheckpointPersisted) { + // We intentionally grab the checkpoint from storage rather than relying on the one referenced by currentState. This is so that + // we mirror exactly what happens when restarting the node. + val serializedCheckpoint = database.transaction { checkpointStorage.getCheckpoint(flowId) } + if (serializedCheckpoint == null) { + logger.error("Unable to find database checkpoint for flow $flowId. Something is very wrong. The flow will not retry.") return } - val flow = if (currentState.isAnyCheckpointPersisted) { - // We intentionally grab the checkpoint from storage rather than relying on the one referenced by currentState. This is so that - // we mirror exactly what happens when restarting the node. - val serializedCheckpoint = checkpointStorage.getCheckpoint(flowId) - if (serializedCheckpoint == null) { - logger.error("Unable to find database checkpoint for flow $flowId. Something is very wrong. The flow will not retry.") - return - } - // Resurrect flow - createFlowFromCheckpoint( - id = flowId, - serializedCheckpoint = serializedCheckpoint, - initialDeduplicationHandler = null, - isAnyCheckpointPersisted = true, - isStartIdempotent = false - ) ?: return - } else { - // Just flow initiation message - null + // Resurrect flow + createFlowFromCheckpoint( + id = flowId, + serializedCheckpoint = serializedCheckpoint, + initialDeduplicationHandler = null, + isAnyCheckpointPersisted = true, + isStartIdempotent = false + ) ?: return + } else { + // Just flow initiation message + null + } + mutex.locked { + if (stopping) { + return } - mutex.locked { - if (stopping) { - return - } - // Remove any sessions the old flow has. - for (sessionId in getFlowSessionIds(currentState.checkpoint)) { - sessionToFlow.remove(sessionId) - } - if (flow != null) { - injectOldProgressTracker(currentState.flowLogic.progressTracker, flow.fiber.logic) - addAndStartFlow(flowId, flow) - } - // Deliver all the external events from the old flow instance. - val unprocessedExternalEvents = mutableListOf() - 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) - } + // Remove any sessions the old flow has. + for (sessionId in getFlowSessionIds(currentState.checkpoint)) { + sessionToFlow.remove(sessionId) + } + if (flow != null) { + injectOldProgressTracker(currentState.flowLogic.progressTracker, flow.fiber.logic) + addAndStartFlow(flowId, flow) + } + // Deliver all the external events from the old flow instance. + val unprocessedExternalEvents = mutableListOf() + 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) } - } catch (e: Exception) { - // Failed to retry - manually put the flow in for observation rather than - // relying on the [HospitalisingInterceptor] to do so - val exceptions = (currentState.checkpoint.errorState as? ErrorState.Errored) - ?.errors - ?.map { it.exception } - ?.plus(e) ?: emptyList() - logger.info("Failed to retry flow $flowId, keeping in for observation and aborting") - flowHospital.forceIntoOvernightObservation(flowId, exceptions) - throw e } } @@ -589,7 +577,8 @@ class SingleThreadedStateMachineManager( // Load the flow's checkpoint // The checkpoint will be missing if the flow failed before persisting the original checkpoint // CORDA-3359 - Do not start/retry a flow that failed after deleting its checkpoint (the whole of the flow might replay) - checkpointStorage.getCheckpoint(flowId)?.let { serializedCheckpoint -> + val existingCheckpoint = database.transaction { checkpointStorage.getCheckpoint(flowId) } + existingCheckpoint?.let { serializedCheckpoint -> val checkpoint = tryCheckpointDeserialize(serializedCheckpoint, flowId) if (checkpoint == null) { return openFuture>().mapError { @@ -919,6 +908,8 @@ class SingleThreadedStateMachineManager( (exception as? FlowException)?.originalErrorId = flowError.errorId flow.resultFuture.setException(exception) lastState.flowLogic.progressTracker?.endWithError(exception) + // Complete the started future, needed when the flow fails during flow init (before completing an [UnstartedFlowTransition]) + startedFutures.remove(flow.fiber.id)?.set(Unit) changesPublisher.onNext(StateMachineManager.Change.Removed(lastState.flowLogic, Try.Failure(exception))) } diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/StaffedFlowHospital.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/StaffedFlowHospital.kt index dbaeb0e586..54d88983b4 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/StaffedFlowHospital.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/StaffedFlowHospital.kt @@ -31,6 +31,7 @@ import java.time.Instant import java.util.* import java.util.concurrent.ConcurrentHashMap import javax.persistence.PersistenceException +import kotlin.collections.HashMap import kotlin.concurrent.timerTask import kotlin.math.pow @@ -51,15 +52,24 @@ class StaffedFlowHospital(private val flowMessaging: FlowMessaging, DatabaseEndocrinologist, TransitionErrorGeneralPractitioner, SedationNurse, - NotaryDoctor + NotaryDoctor, + ResuscitationSpecialist ) + private const val MAX_BACKOFF_TIME = 110.0 // Totals to 2 minutes when calculating the backoff time + @VisibleForTesting val onFlowKeptForOvernightObservation = mutableListOf<(id: StateMachineRunId, by: List) -> Unit>() @VisibleForTesting val onFlowDischarged = mutableListOf<(id: StateMachineRunId, by: List) -> Unit>() + @VisibleForTesting + val onFlowErrorPropagated = mutableListOf<(id: StateMachineRunId, by: List) -> Unit>() + + @VisibleForTesting + val onFlowResuscitated = mutableListOf<(id: StateMachineRunId, by: List, outcome: Outcome) -> Unit>() + @VisibleForTesting val onFlowAdmitted = mutableListOf<(id: StateMachineRunId) -> Unit>() } @@ -164,39 +174,11 @@ class StaffedFlowHospital(private val flowMessaging: FlowMessaging, } /** - * Forces the flow to be kept in for overnight observation by the hospital. A flow must already exist inside the hospital - * and have existing medical records for it to be moved to overnight observation. If it does not meet these criteria then - * an [IllegalArgumentException] will be thrown. - * - * @param id The [StateMachineRunId] of the flow that you are trying to force into observation - * @param errors The errors to include in the new medical record - */ - fun forceIntoOvernightObservation(id: StateMachineRunId, errors: List) { - mutex.locked { - // If a flow does not meet the criteria below, then it has moved into an invalid state or the function is being - // called from an incorrect location. The assertions below should error out the flow if they are not true. - requireNotNull(flowsInHospital[id]) { "Flow must already be in the hospital before forcing into overnight observation" } - val history = requireNotNull(flowPatients[id]) { "Flow must already have history before forcing into overnight observation" } - // Use the last staff member that last discharged the flow as the current staff member - val record = history.records.last().copy( - time = clock.instant(), - errors = errors, - outcome = Outcome.OVERNIGHT_OBSERVATION - ) - onFlowKeptForOvernightObservation.forEach { hook -> hook.invoke(id, record.by.map { it.toString() }) } - history.records += record - recordsPublisher.onNext(record) - } - } - - - /** - * Request treatment for the [flowFiber]. A flow can only be added to the hospital if they are not already being - * treated. + * Request treatment for the [flowFiber]. */ fun requestTreatment(flowFiber: FlowFiber, currentState: StateMachineState, errors: List) { - // Only treat flows that are not already in the hospital - if (!currentState.isRemoved && flowsInHospital.putIfAbsent(flowFiber.id, flowFiber) == null) { + if (!currentState.isRemoved) { + flowsInHospital[flowFiber.id] = flowFiber admit(flowFiber, currentState, errors) } } @@ -216,20 +198,30 @@ class StaffedFlowHospital(private val flowMessaging: FlowMessaging, Diagnosis.DISCHARGE -> { val backOff = calculateBackOffForChronicCondition(report, medicalHistory, currentState) log.info("Flow error discharged from hospital (delay ${backOff.seconds}s) by ${report.by} (error was ${report.error.message})") - onFlowDischarged.forEach { hook -> hook.invoke(flowFiber.id, report.by.map{it.toString()}) } + onFlowDischarged.forEach { hook -> hook.invoke(flowFiber.id, report.by.map { it.toString() }) } Triple(Outcome.DISCHARGE, Event.RetryFlowFromSafePoint, backOff) } Diagnosis.OVERNIGHT_OBSERVATION -> { log.info("Flow error kept for overnight observation by ${report.by} (error was ${report.error.message})") // We don't schedule a next event for the flow - it will automatically retry from its checkpoint on node restart - onFlowKeptForOvernightObservation.forEach { hook -> hook.invoke(flowFiber.id, report.by.map{it.toString()}) } + onFlowKeptForOvernightObservation.forEach { hook -> hook.invoke(flowFiber.id, report.by.map { it.toString() }) } Triple(Outcome.OVERNIGHT_OBSERVATION, null, 0.seconds) } Diagnosis.NOT_MY_SPECIALTY, Diagnosis.TERMINAL -> { // None of the staff care for these errors, or someone decided it is a terminal condition, so we let them propagate log.info("Flow error allowed to propagate", report.error) + onFlowErrorPropagated.forEach { hook -> hook.invoke(flowFiber.id, report.by.map { it.toString() }) } Triple(Outcome.UNTREATABLE, Event.StartErrorPropagation, 0.seconds) } + Diagnosis.RESUSCITATE -> { + // reschedule the last outcome as it failed to process it + // do a 0.seconds backoff in dev mode? / when coming from the driver? make it configurable? + val backOff = calculateBackOffForResuscitation(medicalHistory, currentState) + val outcome = medicalHistory.records.last().outcome + log.info("Flow error to be resuscitated, rescheduling previous outcome - $outcome (delay ${backOff.seconds}s) by ${report.by} (error was ${report.error.message})") + onFlowResuscitated.forEach { hook -> hook.invoke(flowFiber.id, report.by.map { it.toString() }, outcome) } + Triple(outcome, outcome.event, backOff) + } } val record = MedicalRecord.Flow(time, flowFiber.id, currentState.checkpoint.numberOfSuspends, errors, report.by, outcome) @@ -249,18 +241,29 @@ class StaffedFlowHospital(private val flowMessaging: FlowMessaging, } } - private fun calculateBackOffForChronicCondition(report: ConsultationReport, medicalHistory: FlowMedicalHistory, currentState: StateMachineState): Duration { - return report.by.firstOrNull { it is Chronic }?.let { chronicStaff -> - return medicalHistory.timesDischargedForTheSameThing(chronicStaff, currentState).let { - if (it == 0) { - 0.seconds - } else { - maxOf(10, (10 + (Math.random()) * (10 * 1.5.pow(it)) / 2).toInt()).seconds - } - } + private fun calculateBackOffForChronicCondition( + report: ConsultationReport, + medicalHistory: FlowMedicalHistory, + currentState: StateMachineState + ): Duration { + return report.by.firstOrNull { it is Chronic }?.let { staff -> + calculateBackOff(medicalHistory.timesDischargedForTheSameThing(staff, currentState)) } ?: 0.seconds } + private fun calculateBackOffForResuscitation( + medicalHistory: FlowMedicalHistory, + currentState: StateMachineState + ): Duration = calculateBackOff(medicalHistory.timesResuscitated(currentState)) + + private fun calculateBackOff(timesDiagnosisGiven: Int): Duration { + return if (timesDiagnosisGiven == 0) { + 0.seconds + } else { + maxOf(10, (10 + (Math.random()) * minOf(MAX_BACKOFF_TIME, (10 * 1.5.pow(timesDiagnosisGiven)) / 2)).toInt()).seconds + } + } + private fun consultStaff(flowFiber: FlowFiber, currentState: StateMachineState, errors: List, @@ -318,6 +321,11 @@ class StaffedFlowHospital(private val flowMessaging: FlowMessaging, return records.count { it.outcome == Outcome.DISCHARGE && by in it.by && it.suspendCount == lastAdmittanceSuspendCount } } + fun timesResuscitated(currentState: StateMachineState): Int { + val lastAdmittanceSuspendCount = currentState.checkpoint.numberOfSuspends + return records.count { ResuscitationSpecialist in it.by && it.suspendCount == lastAdmittanceSuspendCount } + } + override fun toString(): String = "${this.javaClass.simpleName}(records = $records)" } @@ -351,10 +359,16 @@ class StaffedFlowHospital(private val flowMessaging: FlowMessaging, } } - enum class Outcome { DISCHARGE, OVERNIGHT_OBSERVATION, UNTREATABLE } + enum class Outcome(val event: Event?) { + DISCHARGE(Event.RetryFlowFromSafePoint), + OVERNIGHT_OBSERVATION(null), + UNTREATABLE(Event.StartErrorPropagation) + } /** The order of the enum values are in priority order. */ enum class Diagnosis { + /** Retry the last outcome/diagnosis **/ + RESUSCITATE, /** The flow should not see other staff members */ TERMINAL, /** Retry from last safe point. */ @@ -369,6 +383,11 @@ class StaffedFlowHospital(private val flowMessaging: FlowMessaging, fun consult(flowFiber: FlowFiber, currentState: StateMachineState, newError: Throwable, history: FlowMedicalHistory): Diagnosis } + /** + * The [Chronic] interface relates to [Staff] that return diagnoses that can be constantly be diagnosed if the flow keeps returning to + * the hospital. [Chronic] diagnoses apply a backoff before scheduling a new [Event], this prevents a flow from constantly retrying + * without a chance for the underlying issue to resolve itself. + */ interface Chronic /** @@ -539,10 +558,10 @@ class StaffedFlowHospital(private val flowMessaging: FlowMessaging, newError.mentionsThrowable(AsyncOperationTransitionException::class.java) -> Diagnosis.NOT_MY_SPECIALTY history.notDischargedForTheSameThingMoreThan(2, this, currentState) -> Diagnosis.DISCHARGE else -> Diagnosis.OVERNIGHT_OBSERVATION - } + }.also { logDiagnosis(it, newError, flowFiber, history) } } else { Diagnosis.NOT_MY_SPECIALTY - }.also { logDiagnosis(it, newError, flowFiber, history) } + } } private fun logDiagnosis(diagnosis: Diagnosis, newError: Throwable, flowFiber: FlowFiber, history: FlowMedicalHistory) { @@ -593,6 +612,25 @@ class StaffedFlowHospital(private val flowMessaging: FlowMessaging, return Diagnosis.NOT_MY_SPECIALTY } } + + /** + * Handles errors coming from the processing of errors events ([Event.StartErrorPropagation] and [Event.RetryFlowFromSafePoint]), + * returning a [Diagnosis.RESUSCITATE] diagnosis + */ + object ResuscitationSpecialist : Staff { + override fun consult( + flowFiber: FlowFiber, + currentState: StateMachineState, + newError: Throwable, + history: FlowMedicalHistory + ): Diagnosis { + return if (newError is ErrorStateTransitionException) { + Diagnosis.RESUSCITATE + } else { + Diagnosis.NOT_MY_SPECIALTY + } + } + } } private fun Throwable?.mentionsThrowable(exceptionType: Class, errorMessage: String? = null): Boolean { diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/StateTransitionExceptions.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/StateTransitionExceptions.kt index e32014ab18..2e37261c04 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/StateTransitionExceptions.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/StateTransitionExceptions.kt @@ -16,3 +16,5 @@ class StateTransitionException( } class AsyncOperationTransitionException(exception: Exception) : CordaException(exception.message, exception) + +class ErrorStateTransitionException(val exception: Exception) : CordaException(exception.message, exception) \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/TransitionExecutorImpl.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/TransitionExecutorImpl.kt index 07aa887112..e9f9cd8731 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/TransitionExecutorImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/TransitionExecutorImpl.kt @@ -10,6 +10,7 @@ import net.corda.nodeapi.internal.persistence.DatabaseTransactionException import net.corda.nodeapi.internal.persistence.contextDatabase import net.corda.nodeapi.internal.persistence.contextTransactionOrNull import java.security.SecureRandom +import java.sql.SQLException import javax.persistence.OptimisticLockException /** @@ -20,8 +21,8 @@ import javax.persistence.OptimisticLockException * completely aborted to avoid error loops. */ class TransitionExecutorImpl( - val secureRandom: SecureRandom, - val database: CordaPersistence + val secureRandom: SecureRandom, + val database: CordaPersistence ) : TransitionExecutor { override fun forceRemoveFlow(id: StateMachineRunId) {} @@ -32,33 +33,44 @@ class TransitionExecutorImpl( @Suppress("NestedBlockDepth", "ReturnCount") @Suspendable override fun executeTransition( - fiber: FlowFiber, - previousState: StateMachineState, - event: Event, - transition: TransitionResult, - actionExecutor: ActionExecutor + fiber: FlowFiber, + previousState: StateMachineState, + event: Event, + transition: TransitionResult, + actionExecutor: ActionExecutor ): Pair { contextDatabase = database for (action in transition.actions) { try { actionExecutor.executeAction(fiber, action) } catch (exception: Exception) { - contextTransactionOrNull?.close() + rollbackTransactionOnError() if (transition.newState.checkpoint.errorState is ErrorState.Errored) { - // If we errored while transitioning to an error state then we cannot record the additional - // error as that may result in an infinite loop, e.g. error propagation fails -> record error -> propagate fails again. - // Instead we just keep around the old error state and wait for a new schedule, perhaps - // triggered from a flow hospital - log.warn("Error while executing $action during transition to errored state, aborting transition", exception) - // CORDA-3354 - Go to the hospital with the new error that has occurred - // while already in a error state (as this error could be for a different reason) - return Pair(FlowContinuation.Abort, previousState.copy(isFlowResumed = false)) + log.warn("Error while executing $action, with error event $event, updating errored state", exception) + + val newState = previousState.copy( + checkpoint = previousState.checkpoint.copy( + errorState = previousState.checkpoint.errorState.addErrors( + listOf( + FlowError( + secureRandom.nextLong(), + ErrorStateTransitionException(exception) + ) + ) + ) + ), + isFlowResumed = false + ) + + return Pair(FlowContinuation.ProcessEvents, newState) } else { // Otherwise error the state manually keeping the old flow state and schedule a DoRemainingWork // to trigger error propagation - if(previousState.isRemoved && exception is OptimisticLockException) { - log.debug("Flow has been killed and the following error is likely due to the flow's checkpoint being deleted. " + - "Occurred while executing $action, with event $event", exception) + if (log.isDebugEnabled && previousState.isRemoved && exception is OptimisticLockException) { + log.debug( + "Flow has been killed and the following error is likely due to the flow's checkpoint being deleted. " + + "Occurred while executing $action, with event $event", exception + ) } else { log.info("Error while executing $action, with event $event, erroring state", exception) } @@ -76,12 +88,12 @@ class TransitionExecutorImpl( } val newState = previousState.copy( - checkpoint = previousState.checkpoint.copy( - errorState = previousState.checkpoint.errorState.addErrors( - listOf(FlowError(secureRandom.nextLong(), stateTransitionOrDatabaseTransactionException)) - ) - ), - isFlowResumed = false + checkpoint = previousState.checkpoint.copy( + errorState = previousState.checkpoint.errorState.addErrors( + listOf(FlowError(secureRandom.nextLong(), stateTransitionOrDatabaseTransactionException)) + ) + ), + isFlowResumed = false ) fiber.scheduleEvent(Event.DoRemainingWork) return Pair(FlowContinuation.ProcessEvents, newState) @@ -90,4 +102,25 @@ class TransitionExecutorImpl( } return Pair(transition.continuation, transition.newState) } + + private fun rollbackTransactionOnError() { + contextTransactionOrNull?.run { + try { + rollback() + } catch (rollbackException: SQLException) { + log.info( + "Error rolling back database transaction from a previous error, continuing error handling for the original error", + rollbackException + ) + } + try { + close() + } catch (rollbackException: SQLException) { + log.info( + "Error closing database transaction from a previous error, continuing error handling for the original error", + rollbackException + ) + } + } + } } \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/interceptors/HospitalisingInterceptor.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/interceptors/HospitalisingInterceptor.kt index 41798a3b3f..d75c902449 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/interceptors/HospitalisingInterceptor.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/interceptors/HospitalisingInterceptor.kt @@ -17,8 +17,8 @@ import net.corda.node.services.statemachine.transitions.TransitionResult * transition. */ class HospitalisingInterceptor( - private val flowHospital: StaffedFlowHospital, - private val delegate: TransitionExecutor + private val flowHospital: StaffedFlowHospital, + private val delegate: TransitionExecutor ) : TransitionExecutor { override fun forceRemoveFlow(id: StateMachineRunId) { removeFlow(id) @@ -32,11 +32,11 @@ class HospitalisingInterceptor( @Suspendable override fun executeTransition( - fiber: FlowFiber, - previousState: StateMachineState, - event: Event, - transition: TransitionResult, - actionExecutor: ActionExecutor + fiber: FlowFiber, + previousState: StateMachineState, + event: Event, + transition: TransitionResult, + actionExecutor: ActionExecutor ): Pair { // If the fiber's previous state was clean then remove it from the hospital @@ -47,8 +47,8 @@ class HospitalisingInterceptor( val (continuation, nextState) = delegate.executeTransition(fiber, previousState, event, transition, actionExecutor) - if (nextState.checkpoint.errorState is ErrorState.Errored && previousState.checkpoint.errorState is ErrorState.Clean) { - val exceptionsToHandle = nextState.checkpoint.errorState.errors.map { it.exception } + if (canEnterHospital(previousState, nextState)) { + val exceptionsToHandle = (nextState.checkpoint.errorState as ErrorState.Errored).errors.map { it.exception } flowHospital.requestTreatment(fiber, previousState, exceptionsToHandle) } if (nextState.isRemoved) { @@ -56,4 +56,9 @@ class HospitalisingInterceptor( } return Pair(continuation, nextState) } + + private fun canEnterHospital(previousState: StateMachineState, nextState: StateMachineState): Boolean { + return nextState.checkpoint.errorState is ErrorState.Errored + && (previousState.checkpoint.errorState as? ErrorState.Errored)?.errors != nextState.checkpoint.errorState.errors + } } diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/transitions/DoRemainingWorkTransition.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/transitions/DoRemainingWorkTransition.kt index 53214c71fb..fcc5c32be7 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/transitions/DoRemainingWorkTransition.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/transitions/DoRemainingWorkTransition.kt @@ -1,6 +1,8 @@ package net.corda.node.services.statemachine.transitions -import net.corda.node.services.statemachine.* +import net.corda.node.services.statemachine.ErrorState +import net.corda.node.services.statemachine.FlowState +import net.corda.node.services.statemachine.StateMachineState /** * This transition checks the current state of the flow and determines whether anything needs to be done. diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/transitions/TopLevelTransition.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/transitions/TopLevelTransition.kt index 5b7b289286..1b897dc141 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/transitions/TopLevelTransition.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/transitions/TopLevelTransition.kt @@ -53,7 +53,7 @@ class TopLevelTransition( private fun errorTransition(event: Event.Error): TransitionResult { return builder { - freshErrorTransition(event.exception) + freshErrorTransition(event.exception, event.rollback) FlowContinuation.ProcessEvents } } @@ -292,9 +292,7 @@ class TopLevelTransition( private fun retryFlowFromSafePointTransition(startingState: StateMachineState): TransitionResult { return builder { // Need to create a flow from the prior checkpoint or flow initiation. - actions.add(Action.CreateTransaction) actions.add(Action.RetryFlowFromSafePoint(startingState)) - actions.add(Action.CommitTransaction) FlowContinuation.Abort } } diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/transitions/TransitionBuilder.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/transitions/TransitionBuilder.kt index 09336fa96d..7cdc7d24d7 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/transitions/TransitionBuilder.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/transitions/TransitionBuilder.kt @@ -28,12 +28,12 @@ class TransitionBuilder(val context: TransitionContext, initialState: StateMachi * * @param error the error. */ - fun freshErrorTransition(error: Throwable) { + fun freshErrorTransition(error: Throwable, rollback: Boolean = true) { val flowError = FlowError( errorId = (error as? IdentifiableException)?.errorId ?: context.secureRandom.nextLong(), exception = error ) - errorTransition(flowError) + errorTransition(flowError, rollback) } /** @@ -42,7 +42,7 @@ class TransitionBuilder(val context: TransitionContext, initialState: StateMachi * * @param error the error. */ - fun errorsTransition(errors: List) { + fun errorsTransition(errors: List, rollback: Boolean) { currentState = currentState.copy( checkpoint = currentState.checkpoint.copy( errorState = currentState.checkpoint.errorState.addErrors(errors) @@ -50,10 +50,10 @@ class TransitionBuilder(val context: TransitionContext, initialState: StateMachi isFlowResumed = false ) actions.clear() - actions.addAll(arrayOf( - Action.RollbackTransaction, - Action.ScheduleEvent(Event.DoRemainingWork) - )) + if(rollback) { + actions += Action.RollbackTransaction + } + actions += Action.ScheduleEvent(Event.DoRemainingWork) } /** @@ -62,8 +62,8 @@ class TransitionBuilder(val context: TransitionContext, initialState: StateMachi * * @param error the error. */ - fun errorTransition(error: FlowError) { - errorsTransition(listOf(error)) + fun errorTransition(error: FlowError, rollback: Boolean) { + errorsTransition(listOf(error), rollback) } fun resumeFlowLogic(result: Any?): FlowContinuation { From efd633c7b9fe0fb370349bfc5b6851cc228d55c4 Mon Sep 17 00:00:00 2001 From: Dan Newton Date: Tue, 28 Apr 2020 11:20:00 +0100 Subject: [PATCH 38/85] CORDA-3722 withEntityManager can rollback its session (#6187) * CORDA-3722 withEntityManager can rollback its session Improve the handling of database transactions when using `withEntityManager` inside a flow. Extra changes have been included to improve the safety and correctness of Corda around handling database transactions. This focuses on allowing flows to catch errors that occur inside an entity manager and handle them accordingly. Errors can be caught in two places: - Inside `withEntityManager` - Outside `withEntityManager` Further changes have been included to ensure that transactions are rolled back correctly. Errors caught inside `withEntityManager` require the flow to manually `flush` the current session (the entity manager's individual session). By manually flushing the session, a `try-catch` block can be placed around the `flush` call, allowing possible exceptions to be caught. Once an error is thrown from a call to `flush`, it is no longer possible to use the same entity manager to trigger any database operations. The only possible option is to rollback the changes from that session. The flow can continue executing updates within the same session but they will never be committed. What happens in this situation should be handled by the flow. Explicitly restricting the scenario requires a lot of effort and code. Instead, we should rely on the developer to control complex workflows. To continue updating the database after an error like this occurs, a new `withEntityManager` block should be used (after catching the previous error). Exceptions can be caught around `withEntityManager` blocks. This allows errors to be handled in the same way as stated above, except the need to manually `flush` the session is removed. `withEntityManager` will automatically `flush` a session if it has not been marked for rollback due to an earlier error. A `try-catch` can then be placed around the whole of the `withEntityManager` block, allowing the error to be caught while not committing any changes to the underlying database transaction. To make `withEntityManager` blocks work like mini database transactions, save points have been utilised. A new savepoint is created when opening a `withEntityManager` block (along with a new session). It is then used as a reference point to rollback to if the session errors and needs to roll back. The savepoint is then released (independently from completing successfully or failing). Using save points means, that either all the statements inside the entity manager are executed, or none of them are. - A new session is created every time an entity manager is requested, but this does not replace the flow's main underlying database session. - `CordaPersistence.transaction` can now determine whether it needs to execute its extra error handling code. This is needed to allow errors escape `withEntityManager` blocks while allowing some of our exception handling around subscribers (in `NodeVaultService`) to continue to work. --- .../kotlin/net/corda/core/node/ServiceHub.kt | 56 ++ .../internal/persistence/CordaPersistence.kt | 31 +- .../persistence/DatabaseTransaction.kt | 18 +- .../persistence/RestrictedEntityManager.kt | 45 +- .../RestrictedEntityManagerTest.kt | 58 ++ .../flows/AbstractFlowEntityManagerTest.kt | 134 +++ .../node/flows/FlowEntityManagerNestedTest.kt | 374 +++++++++ .../flows/FlowEntityManagerStatementTest.kt | 309 +++++++ .../corda/node/flows/FlowEntityManagerTest.kt | 763 ++++++++++++++++++ .../net/corda/node/internal/AbstractNode.kt | 48 +- .../statemachine/ActionExecutorImpl.kt | 5 +- .../AppendOnlyPersistentMapTest.kt | 3 + .../node/services/vault/VaultFlowTest.kt | 9 +- .../net/corda/testing/node/MockServices.kt | 4 +- 14 files changed, 1832 insertions(+), 25 deletions(-) create mode 100644 node-api/src/test/kotlin/net/corda/nodeapi/internal/persistence/RestrictedEntityManagerTest.kt create mode 100644 node/src/integration-test/kotlin/net/corda/node/flows/AbstractFlowEntityManagerTest.kt create mode 100644 node/src/integration-test/kotlin/net/corda/node/flows/FlowEntityManagerNestedTest.kt create mode 100644 node/src/integration-test/kotlin/net/corda/node/flows/FlowEntityManagerStatementTest.kt create mode 100644 node/src/integration-test/kotlin/net/corda/node/flows/FlowEntityManagerTest.kt diff --git a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt index 0e500dd4bb..06f37b4eec 100644 --- a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt +++ b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt @@ -380,6 +380,26 @@ interface ServiceHub : ServicesForResolution { * When used within a flow, this session automatically forms part of the enclosing flow transaction boundary, * and thus queryable data will include everything committed as of the last checkpoint. * + * We want to make sure users have a restricted access to administrative functions, this function will return a [RestrictedConnection] instance. + * The blocked methods are the following: + * - abort(executor: Executor?) + * - clearWarnings() + * - close() + * - commit() + * - setSavepoint() + * - setSavepoint(name : String?) + * - releaseSavepoint(savepoint: Savepoint?) + * - rollback() + * - rollback(savepoint: Savepoint?) + * - setCatalog(catalog : String?) + * - setTransactionIsolation(level: Int) + * - setTypeMap(map: MutableMap>?) + * - setHoldability(holdability: Int) + * - setSchema(schema: String?) + * - setNetworkTimeout(executor: Executor?, milliseconds: Int) + * - setAutoCommit(autoCommit: Boolean) + * - setReadOnly(readOnly: Boolean) + * * @throws IllegalStateException if called outside of a transaction. * @return A [Connection] */ @@ -393,6 +413,24 @@ interface ServiceHub : ServicesForResolution { * NOTE: Suspendable flow operations such as send, receive, subFlow and sleep, cannot be called within the lambda. * * @param block a lambda function with access to an [EntityManager]. + * + * We want to make sure users have a restricted access to administrative functions. + * The following methods are blocked: + * - close() + * - unwrap(cls: Class?) + * - getDelegate(): Any + * - getMetamodel() + * - joinTransaction() + * - lock(entity: Any?, lockMode: LockModeType?) + * - lock(entity: Any?, lockMode: LockModeType?, properties: MutableMap?) + * - setProperty(propertyName: String?, value: Any?) + * + * getTransaction returns a [RestrictedEntityTransaction] to prevent unsafe manipulation of a flow's underlying + * database transaction. + * The following methods are blocked: + * - begin() + * - commit() + * - rollback() */ fun withEntityManager(block: EntityManager.() -> T): T @@ -404,6 +442,24 @@ interface ServiceHub : ServicesForResolution { * NOTE: Suspendable flow operations such as send, receive, subFlow and sleep, cannot be called within the lambda. * * @param block a lambda function with access to an [EntityManager]. + * + * We want to make sure users have a restricted access to administrative functions. + * The following methods are blocked: + * - close() + * - unwrap(cls: Class?) + * - getDelegate(): Any + * - getMetamodel() + * - joinTransaction() + * - lock(entity: Any?, lockMode: LockModeType?) + * - lock(entity: Any?, lockMode: LockModeType?, properties: MutableMap?) + * - setProperty(propertyName: String?, value: Any?) + * + * getTransaction returns a [RestrictedEntityTransaction] to prevent unsafe manipulation of a flow's underlying + * database transaction. + * The following methods are blocked: + * - begin() + * - commit() + * - rollback() */ fun withEntityManager(block: Consumer) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/CordaPersistence.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/CordaPersistence.kt index 4efaff19b3..d77f41081b 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/CordaPersistence.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/CordaPersistence.kt @@ -211,14 +211,15 @@ class CordaPersistence( * @param isolationLevel isolation level for the transaction. * @param statement to be executed in the scope of this transaction. */ - fun transaction(isolationLevel: TransactionIsolationLevel, statement: DatabaseTransaction.() -> T): T = - transaction(isolationLevel, 2, false, statement) + fun transaction(isolationLevel: TransactionIsolationLevel, useErrorHandler: Boolean, statement: DatabaseTransaction.() -> T): T = + transaction(isolationLevel, 2, false, useErrorHandler, statement) /** * Executes given statement in the scope of transaction with the transaction level specified at the creation time. * @param statement to be executed in the scope of this transaction. */ - fun transaction(statement: DatabaseTransaction.() -> T): T = transaction(defaultIsolationLevel, statement) + @JvmOverloads + fun transaction(useErrorHandler: Boolean = true, statement: DatabaseTransaction.() -> T): T = transaction(defaultIsolationLevel, useErrorHandler, statement) /** * Executes given statement in the scope of transaction, with the given isolation level. @@ -228,7 +229,7 @@ class CordaPersistence( * @param statement to be executed in the scope of this transaction. */ fun transaction(isolationLevel: TransactionIsolationLevel, recoverableFailureTolerance: Int, - recoverAnyNestedSQLException: Boolean, statement: DatabaseTransaction.() -> T): T { + recoverAnyNestedSQLException: Boolean, useErrorHandler: Boolean, statement: DatabaseTransaction.() -> T): T { _contextDatabase.set(this) val outer = contextTransactionOrNull return if (outer != null) { @@ -237,26 +238,34 @@ class CordaPersistence( // previously been created by the flow state machine in ActionExecutorImpl#executeCreateTransaction // b. exceptions coming out from top level transactions are already being handled in CordaPersistence#inTopLevelTransaction // i.e. roll back and close the transaction - try { + if(useErrorHandler) { + outer.withErrorHandler(statement) + } else { outer.statement() - } catch (e: Exception) { - if (e is SQLException || e is PersistenceException || e is HospitalizeFlowException) { - outer.errorHandler(e) - } - throw e } } else { inTopLevelTransaction(isolationLevel, recoverableFailureTolerance, recoverAnyNestedSQLException, statement) } } + private fun DatabaseTransaction.withErrorHandler(statement: DatabaseTransaction.() -> T): T { + return try { + statement() + } catch (e: Exception) { + if ((e is SQLException || e is PersistenceException || e is HospitalizeFlowException)) { + errorHandler(e) + } + throw e + } + } + /** * Executes given statement in the scope of transaction with the transaction level specified at the creation time. * @param statement to be executed in the scope of this transaction. * @param recoverableFailureTolerance number of transaction commit retries for SQL while SQL exception is encountered. */ fun transaction(recoverableFailureTolerance: Int, statement: DatabaseTransaction.() -> T): T { - return transaction(defaultIsolationLevel, recoverableFailureTolerance, false, statement) + return transaction(defaultIsolationLevel, recoverableFailureTolerance, false, false, statement) } private fun inTopLevelTransaction(isolationLevel: TransactionIsolationLevel, recoverableFailureTolerance: Int, diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/DatabaseTransaction.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/DatabaseTransaction.kt index 3fb6b682ff..16c5857ae9 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/DatabaseTransaction.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/DatabaseTransaction.kt @@ -40,9 +40,13 @@ class DatabaseTransaction( } // Returns a delegate which overrides certain operations that we do not want CorDapp developers to call. - val restrictedEntityManager: RestrictedEntityManager by lazy { - val entityManager = session as EntityManager - RestrictedEntityManager(entityManager) + + val entityManager: EntityManager get() { + // Always retrieve new session ([Session] implements [EntityManager]) + // Note, this does not replace the top level hibernate session + val session = database.entityManagerFactory.withOptions().connection(connection).openSession() + session.beginTransaction() + return session } val session: Session by sessionDelegate @@ -74,6 +78,10 @@ class DatabaseTransaction( throw DatabaseTransactionException(it) } if (sessionDelegate.isInitialized()) { + // The [sessionDelegate] must be initialised otherwise calling [entityManager] will cause an exception + if(session.transaction.rollbackOnly) { + throw RolledBackDatabaseSessionException() + } hibernateTransaction.commit() } connection.commit() @@ -130,4 +138,6 @@ class DatabaseTransaction( /** * Wrapper exception, for any exception registered as [DatabaseTransaction.firstExceptionInDatabaseTransaction]. */ -class DatabaseTransactionException(override val cause: Throwable): CordaRuntimeException(cause.message, cause) \ No newline at end of file +class DatabaseTransactionException(override val cause: Throwable): CordaRuntimeException(cause.message, cause) + +class RolledBackDatabaseSessionException : CordaRuntimeException("Attempted to commit database transaction marked for rollback") \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/RestrictedEntityManager.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/RestrictedEntityManager.kt index 46df399b6c..befd492278 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/RestrictedEntityManager.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/RestrictedEntityManager.kt @@ -7,13 +7,54 @@ import javax.persistence.EntityManager */ class RestrictedEntityManager(private val delegate: EntityManager) : EntityManager by delegate { + override fun getTransaction(): EntityTransaction { + return RestrictedEntityTransaction(delegate.transaction) + } + override fun close() { throw UnsupportedOperationException("This method cannot be called via ServiceHub.withEntityManager.") } - override fun clear() { + override fun unwrap(cls: Class?): T { throw UnsupportedOperationException("This method cannot be called via ServiceHub.withEntityManager.") } - // TODO: Figure out which other methods on EntityManager need to be blocked? + override fun getDelegate(): Any { + throw UnsupportedOperationException("This method cannot be called via ServiceHub.withEntityManager.") + } + + override fun getMetamodel(): Metamodel? { + throw UnsupportedOperationException("This method cannot be called via ServiceHub.withEntityManager.") + } + + override fun joinTransaction() { + throw UnsupportedOperationException("This method cannot be called via ServiceHub.withEntityManager.") + } + + override fun lock(entity: Any?, lockMode: LockModeType?) { + throw UnsupportedOperationException("This method cannot be called via ServiceHub.withEntityManager.") + } + + override fun lock(entity: Any?, lockMode: LockModeType?, properties: MutableMap?) { + throw UnsupportedOperationException("This method cannot be called via ServiceHub.withEntityManager.") + } + + override fun setProperty(propertyName: String?, value: Any?) { + throw UnsupportedOperationException("This method cannot be called via ServiceHub.withEntityManager.") + } +} + +class RestrictedEntityTransaction(private val delegate: EntityTransaction) : EntityTransaction by delegate { + + override fun rollback() { + throw UnsupportedOperationException("This method cannot be called via ServiceHub.withEntityManager.") + } + + override fun commit() { + throw UnsupportedOperationException("This method cannot be called via ServiceHub.withEntityManager.") + } + + override fun begin() { + throw UnsupportedOperationException("This method cannot be called via ServiceHub.withEntityManager.") + } } \ No newline at end of file diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/persistence/RestrictedEntityManagerTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/persistence/RestrictedEntityManagerTest.kt new file mode 100644 index 0000000000..6f53ade01c --- /dev/null +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/persistence/RestrictedEntityManagerTest.kt @@ -0,0 +1,58 @@ +package net.corda.nodeapi.internal.persistence + +import com.nhaarman.mockito_kotlin.doReturn +import com.nhaarman.mockito_kotlin.mock +import com.nhaarman.mockito_kotlin.whenever +import org.junit.Test +import javax.persistence.EntityManager +import javax.persistence.EntityTransaction +import javax.persistence.LockModeType +import kotlin.test.assertTrue + +class RestrictedEntityManagerTest { + private val entitymanager = mock() + private val transaction = mock() + private val restrictedEntityManager = RestrictedEntityManager(entitymanager) + + @Test(expected = UnsupportedOperationException::class, timeout=300_000) + fun testClose() { + restrictedEntityManager.close() + } + + @Test(timeout = 300_000) + fun testClear() { + restrictedEntityManager.clear() + } + + @Test(expected = UnsupportedOperationException::class, timeout=300_000) + fun testGetMetaModel() { + restrictedEntityManager.getMetamodel() + } + + @Test(timeout = 300_000) + fun testGetTransaction() { + whenever(entitymanager.transaction).doReturn(transaction) + assertTrue(restrictedEntityManager.transaction is RestrictedEntityTransaction) + } + + @Test(expected = UnsupportedOperationException::class, timeout=300_000) + fun testJoinTransaction() { + restrictedEntityManager.joinTransaction() + } + + @Test(expected = UnsupportedOperationException::class, timeout=300_000) + fun testLockWithTwoParameters() { + restrictedEntityManager.lock(Object(), LockModeType.OPTIMISTIC) + } + + @Test(expected = UnsupportedOperationException::class, timeout=300_000) + fun testLockWithThreeParameters() { + val map: MutableMap = mutableMapOf() + restrictedEntityManager.lock(Object(), LockModeType.OPTIMISTIC,map) + } + + @Test(expected = UnsupportedOperationException::class, timeout=300_000) + fun testSetProperty() { + restrictedEntityManager.setProperty("number", 12) + } +} \ No newline at end of file diff --git a/node/src/integration-test/kotlin/net/corda/node/flows/AbstractFlowEntityManagerTest.kt b/node/src/integration-test/kotlin/net/corda/node/flows/AbstractFlowEntityManagerTest.kt new file mode 100644 index 0000000000..46faabf82d --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/node/flows/AbstractFlowEntityManagerTest.kt @@ -0,0 +1,134 @@ +package net.corda.node.flows + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.StartableByRPC +import net.corda.core.messaging.CordaRPCOps +import net.corda.core.messaging.startFlow +import net.corda.core.schemas.MappedSchema +import net.corda.core.serialization.CordaSerializable +import net.corda.core.utilities.getOrThrow +import net.corda.core.utilities.seconds +import net.corda.node.services.statemachine.StaffedFlowHospital +import org.junit.Before +import java.util.concurrent.Semaphore +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.Id +import javax.persistence.Table +import kotlin.test.assertEquals + +abstract class AbstractFlowEntityManagerTest { + + protected companion object { + + const val TABLE_NAME = "entity_manager_custom_table" + + val entityWithIdOne = CustomTableEntity(1, "Dan", "This won't work") + val anotherEntityWithIdOne = CustomTableEntity(1, "Rick", "I'm pretty sure this will work") + val entityWithIdTwo = CustomTableEntity(2, "Ivan", "This will break existing CorDapps") + val entityWithIdThree = CustomTableEntity(3, "Some other guy", "What am I doing here?") + } + + @CordaSerializable + enum class CommitStatus { INTERMEDIATE_COMMIT, NO_INTERMEDIATE_COMMIT } + + @Before + open fun before() { + StaffedFlowHospital.onFlowDischarged.clear() + StaffedFlowHospital.onFlowKeptForOvernightObservation.clear() + StaffedFlowHospital.onFlowKeptForOvernightObservation.clear() + } + + protected inline fun > CordaRPCOps.expectFlowFailureAndAssertCreatedEntities( + crossinline flow: (CommitStatus) -> R, + commitStatus: CommitStatus, + numberOfDischarges: Int, + numberOfExpectedEntities: Int + ): Int { + var counter = 0 + StaffedFlowHospital.onFlowDischarged.add { _, _ -> ++counter } + val lock = Semaphore(0) + StaffedFlowHospital.onFlowKeptForOvernightObservation.add { _, _ -> lock.release() } + startFlow(flow, commitStatus) + lock.acquire() + assertEquals( + numberOfDischarges, + counter, + "[$commitStatus] expected the flow to be discharged from hospital $numberOfDischarges time(s)" + ) + val numberOfEntities = startFlow(::GetCustomEntities).returnValue.getOrThrow().size + assertEquals( + numberOfExpectedEntities, + numberOfEntities, + "[$commitStatus] expected $numberOfExpectedEntities to be saved" + ) + startFlow(::DeleteCustomEntities).returnValue.getOrThrow(30.seconds) + return numberOfEntities + } + + protected inline fun > CordaRPCOps.expectFlowSuccessAndAssertCreatedEntities( + crossinline flow: (CommitStatus) -> R, + commitStatus: CommitStatus, + numberOfDischarges: Int, + numberOfExpectedEntities: Int + ): Int { + var counter = 0 + StaffedFlowHospital.onFlowDischarged.add { _, _ -> ++counter } + startFlow(flow, commitStatus).returnValue.getOrThrow(30.seconds) + assertEquals( + numberOfDischarges, + counter, + "[$commitStatus] expected the flow to be discharged from hospital $numberOfDischarges time(s)" + ) + val numberOfEntities = startFlow(::GetCustomEntities).returnValue.getOrThrow().size + assertEquals( + numberOfExpectedEntities, + numberOfEntities, + "[$commitStatus] expected $numberOfExpectedEntities to be saved" + ) + startFlow(::DeleteCustomEntities).returnValue.getOrThrow(30.seconds) + return numberOfEntities + } + + @StartableByRPC + class GetCustomEntities : FlowLogic>() { + @Suspendable + override fun call(): List { + return serviceHub.withEntityManager { + val criteria = criteriaBuilder.createQuery(CustomTableEntity::class.java) + criteria.select(criteria.from(CustomTableEntity::class.java)) + createQuery(criteria).resultList + } + } + } + + @StartableByRPC + class DeleteCustomEntities : FlowLogic() { + @Suspendable + override fun call() { + serviceHub.withEntityManager { + val delete = criteriaBuilder.createCriteriaDelete(CustomTableEntity::class.java) + delete.from(CustomTableEntity::class.java) + createQuery(delete).executeUpdate() + } + } + } + + @Entity + @Table(name = TABLE_NAME) + @CordaSerializable + data class CustomTableEntity constructor( + @Id + @Column(name = "id", nullable = false) + var id: Int, + @Column(name = "name", nullable = false) + var name: String, + @Column(name = "quote", nullable = false) + var quote: String + ) + + object CustomSchema + + object CustomMappedSchema : MappedSchema(CustomSchema::class.java, 1, listOf(CustomTableEntity::class.java)) +} \ No newline at end of file diff --git a/node/src/integration-test/kotlin/net/corda/node/flows/FlowEntityManagerNestedTest.kt b/node/src/integration-test/kotlin/net/corda/node/flows/FlowEntityManagerNestedTest.kt new file mode 100644 index 0000000000..73930aec6c --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/node/flows/FlowEntityManagerNestedTest.kt @@ -0,0 +1,374 @@ +package net.corda.node.flows + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.StartableByRPC +import net.corda.core.messaging.startFlow +import net.corda.core.utilities.getOrThrow +import net.corda.core.utilities.millis +import net.corda.core.utilities.seconds +import net.corda.node.services.statemachine.StaffedFlowHospital +import net.corda.testing.core.ALICE_NAME +import net.corda.testing.driver.DriverParameters +import net.corda.testing.driver.driver +import org.junit.Test +import javax.persistence.PersistenceException +import kotlin.test.assertEquals + +class FlowEntityManagerNestedTest : AbstractFlowEntityManagerTest() { + + @Test(timeout = 300_000) + fun `entity manager inside an entity manager saves all data`() { + var counter = 0 + StaffedFlowHospital.onFlowDischarged.add { _, _ -> ++counter } + driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { + + val alice = startNode(providedName = ALICE_NAME).getOrThrow() + + alice.rpc.startFlow(::EntityManagerInsideAnEntityManagerFlow).returnValue.getOrThrow(20.seconds) + assertEquals(0, counter) + val entities = alice.rpc.startFlow(::GetCustomEntities).returnValue.getOrThrow() + assertEquals(2, entities.size) + } + } + + @Test(timeout = 300_000) + fun `entity manager inside an entity manager that throws an error does not save any data`() { + driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { + + val alice = startNode(providedName = ALICE_NAME).getOrThrow() + alice.rpc.expectFlowFailureAndAssertCreatedEntities( + flow = ::EntityManagerInsideAnEntityManagerThatThrowsAnExceptionFlow, + commitStatus = CommitStatus.NO_INTERMEDIATE_COMMIT, + numberOfDischarges = 3, + numberOfExpectedEntities = 0 + ) + alice.rpc.expectFlowFailureAndAssertCreatedEntities( + flow = ::EntityManagerInsideAnEntityManagerThatThrowsAnExceptionFlow, + commitStatus = CommitStatus.INTERMEDIATE_COMMIT, + numberOfDischarges = 3, + numberOfExpectedEntities = 1 + ) + } + } + + @Test(timeout = 300_000) + fun `entity manager that saves an entity with an entity manager inside it that throws an error after saving the entity does not save any data`() { + driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { + + val alice = startNode(providedName = ALICE_NAME).getOrThrow() + alice.rpc.expectFlowFailureAndAssertCreatedEntities( + flow = ::EntityManagerThatSavesAnEntityWithAnEntityManagerInsideItThatThrowsAnExceptionAfterSavingFlow, + commitStatus = CommitStatus.NO_INTERMEDIATE_COMMIT, + numberOfDischarges = 3, + numberOfExpectedEntities = 0 + ) + alice.rpc.expectFlowFailureAndAssertCreatedEntities( + flow = ::EntityManagerThatSavesAnEntityWithAnEntityManagerInsideItThatThrowsAnExceptionAfterSavingFlow, + commitStatus = CommitStatus.INTERMEDIATE_COMMIT, + numberOfDischarges = 3, + numberOfExpectedEntities = 1 + ) + } + } + + @Test(timeout = 300_000) + fun `entity manager that saves an entity with an entity manager inside it that throws an error and catching it around the entity manager after saving the entity saves the data from the external entity manager`() { + driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { + + val alice = startNode(providedName = ALICE_NAME).getOrThrow() + alice.rpc.expectFlowSuccessAndAssertCreatedEntities( + flow = ::EntityManagerThatSavesAnEntityWithAnEntityManagerInsideItThatThrowsAnExceptionAndCatchesAroundTheEntityManagerAfterSavingFlow, + commitStatus = CommitStatus.NO_INTERMEDIATE_COMMIT, + numberOfDischarges = 0, + numberOfExpectedEntities = 2 + ) + alice.rpc.expectFlowSuccessAndAssertCreatedEntities( + flow = ::EntityManagerThatSavesAnEntityWithAnEntityManagerInsideItThatThrowsAnExceptionAndCatchesAroundTheEntityManagerAfterSavingFlow, + commitStatus = CommitStatus.INTERMEDIATE_COMMIT, + numberOfDischarges = 0, + numberOfExpectedEntities = 2 + ) + } + } + + @Test(timeout = 300_000) + fun `entity manager that saves an entity with an entity manager inside it that throws an error and catching it inside the entity manager after saving the entity saves the data from the external entity manager`() { + driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { + + val alice = startNode(providedName = ALICE_NAME).getOrThrow() + alice.rpc.expectFlowSuccessAndAssertCreatedEntities( + flow = ::EntityManagerThatSavesAnEntityWithAnEntityManagerInsideItThatThrowsAnExceptionAndCatchesInsideTheEntityManagerAfterSavingFlow, + commitStatus = CommitStatus.NO_INTERMEDIATE_COMMIT, + numberOfDischarges = 0, + numberOfExpectedEntities = 2 + ) + alice.rpc.expectFlowSuccessAndAssertCreatedEntities( + flow = ::EntityManagerThatSavesAnEntityWithAnEntityManagerInsideItThatThrowsAnExceptionAndCatchesInsideTheEntityManagerAfterSavingFlow, + commitStatus = CommitStatus.INTERMEDIATE_COMMIT, + numberOfDischarges = 0, + numberOfExpectedEntities = 2 + ) + } + } + + @Test(timeout = 300_000) + fun `entity manager that saves an entity with an entity manager inside it that throws an error and catching it around the entity manager before saving the entity saves the data from the external entity manager`() { + driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { + + val alice = startNode(providedName = ALICE_NAME).getOrThrow() + alice.rpc.expectFlowSuccessAndAssertCreatedEntities( + flow = ::EntityManagerThatSavesAnEntityWithAnEntityManagerInsideItThatThrowsAnExceptionAndCatchesAroundTheEntityManagerBeforeSavingFlow, + commitStatus = CommitStatus.NO_INTERMEDIATE_COMMIT, + numberOfDischarges = 0, + numberOfExpectedEntities = 2 + ) + alice.rpc.expectFlowSuccessAndAssertCreatedEntities( + flow = ::EntityManagerThatSavesAnEntityWithAnEntityManagerInsideItThatThrowsAnExceptionAndCatchesAroundTheEntityManagerBeforeSavingFlow, + commitStatus = CommitStatus.INTERMEDIATE_COMMIT, + numberOfDischarges = 0, + numberOfExpectedEntities = 2 + ) + } + } + + @Test(timeout = 300_000) + fun `entity manager with an entity manager inside it saves an entity, outer throws and catches the error outside itself after saving the entity does not save the data from the internal entity manager`() { + driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { + + val alice = startNode(providedName = ALICE_NAME).getOrThrow() + alice.rpc.expectFlowSuccessAndAssertCreatedEntities( + flow = ::EntityManagerThatSavesAnEntityUsingInternalEntityManagerAndThrowsFromOuterAndCatchesAroundOuterEntityManagerAfterSavingFlow, + commitStatus = CommitStatus.NO_INTERMEDIATE_COMMIT, + numberOfDischarges = 0, + numberOfExpectedEntities = 1 + ) + alice.rpc.expectFlowSuccessAndAssertCreatedEntities( + flow = ::EntityManagerThatSavesAnEntityUsingInternalEntityManagerAndThrowsFromOuterAndCatchesAroundOuterEntityManagerAfterSavingFlow, + commitStatus = CommitStatus.INTERMEDIATE_COMMIT, + numberOfDischarges = 0, + numberOfExpectedEntities = 1 + ) + } + } + + @Test(timeout = 300_000) + fun `entity manager with an entity manager inside it saves an entity, outer throws and catches the error inside itself after saving the entity does not save the data from the internal entity manager`() { + driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { + + val alice = startNode(providedName = ALICE_NAME).getOrThrow() + alice.rpc.expectFlowSuccessAndAssertCreatedEntities( + flow = ::EntityManagerThatSavesAnEntityUsingInternalEntityManagerAndThrowsFromOuterAndCatchesInsideOuterEntityManagerAfterSavingFlow, + commitStatus = CommitStatus.NO_INTERMEDIATE_COMMIT, + numberOfDischarges = 0, + numberOfExpectedEntities = 1 + ) + alice.rpc.expectFlowSuccessAndAssertCreatedEntities( + flow = ::EntityManagerThatSavesAnEntityUsingInternalEntityManagerAndThrowsFromOuterAndCatchesInsideOuterEntityManagerAfterSavingFlow, + commitStatus = CommitStatus.INTERMEDIATE_COMMIT, + numberOfDischarges = 0, + numberOfExpectedEntities = 1 + ) + } + } + + @StartableByRPC + class EntityManagerInsideAnEntityManagerFlow : FlowLogic() { + + @Suspendable + override fun call() { + serviceHub.withEntityManager { + persist(entityWithIdOne) + serviceHub.withEntityManager { + persist(entityWithIdTwo) + } + } + sleep(1.millis) + } + } + + @StartableByRPC + class EntityManagerInsideAnEntityManagerThatThrowsAnExceptionFlow(private val commitStatus: CommitStatus) : FlowLogic() { + + @Suspendable + override fun call() { + serviceHub.withEntityManager { + persist(entityWithIdOne) + } + if (commitStatus == CommitStatus.INTERMEDIATE_COMMIT) { + sleep(1.millis) + } + serviceHub.withEntityManager { + serviceHub.withEntityManager { + persist(anotherEntityWithIdOne) + } + } + sleep(1.millis) + } + } + + @StartableByRPC + class EntityManagerThatSavesAnEntityWithAnEntityManagerInsideItThatThrowsAnExceptionAfterSavingFlow(private val commitStatus: CommitStatus) : + FlowLogic() { + + @Suspendable + override fun call() { + serviceHub.withEntityManager { + persist(entityWithIdOne) + } + if (commitStatus == CommitStatus.INTERMEDIATE_COMMIT) { + sleep(1.millis) + } + serviceHub.withEntityManager { + persist(entityWithIdTwo) + serviceHub.withEntityManager { + persist(anotherEntityWithIdOne) + } + } + sleep(1.millis) + } + } + + @StartableByRPC + class EntityManagerThatSavesAnEntityWithAnEntityManagerInsideItThatThrowsAnExceptionAndCatchesAroundTheEntityManagerAfterSavingFlow( + private val commitStatus: CommitStatus + ) : + FlowLogic() { + + @Suspendable + override fun call() { + serviceHub.withEntityManager { + persist(entityWithIdOne) + } + if (commitStatus == CommitStatus.INTERMEDIATE_COMMIT) { + sleep(1.millis) + } + serviceHub.withEntityManager { + persist(entityWithIdTwo) + try { + serviceHub.withEntityManager { + persist(anotherEntityWithIdOne) + } + } catch (e: PersistenceException) { + logger.info("Caught the exception!") + } + } + sleep(1.millis) + } + } + + @StartableByRPC + class EntityManagerThatSavesAnEntityWithAnEntityManagerInsideItThatThrowsAnExceptionAndCatchesInsideTheEntityManagerAfterSavingFlow( + private val commitStatus: CommitStatus + ) : + FlowLogic() { + + @Suspendable + override fun call() { + serviceHub.withEntityManager { + persist(entityWithIdOne) + } + if (commitStatus == CommitStatus.INTERMEDIATE_COMMIT) { + sleep(1.millis) + } + serviceHub.withEntityManager { + persist(entityWithIdTwo) + serviceHub.withEntityManager { + try { + persist(anotherEntityWithIdOne) + flush() + } catch (e: PersistenceException) { + logger.info("Caught the exception!") + } + } + + } + sleep(1.millis) + } + } + + @StartableByRPC + class EntityManagerThatSavesAnEntityWithAnEntityManagerInsideItThatThrowsAnExceptionAndCatchesAroundTheEntityManagerBeforeSavingFlow( + private val commitStatus: CommitStatus + ) : + FlowLogic() { + + @Suspendable + override fun call() { + serviceHub.withEntityManager { + persist(entityWithIdOne) + } + if (commitStatus == CommitStatus.INTERMEDIATE_COMMIT) { + sleep(1.millis) + } + serviceHub.withEntityManager { + try { + serviceHub.withEntityManager { + persist(anotherEntityWithIdOne) + } + } catch (e: PersistenceException) { + logger.info("Caught the exception!") + } + persist(entityWithIdTwo) + } + sleep(1.millis) + } + } + + @StartableByRPC + class EntityManagerThatSavesAnEntityUsingInternalEntityManagerAndThrowsFromOuterAndCatchesAroundOuterEntityManagerAfterSavingFlow( + private val commitStatus: CommitStatus + ) : + FlowLogic() { + + @Suspendable + override fun call() { + serviceHub.withEntityManager { + persist(entityWithIdOne) + } + if (commitStatus == CommitStatus.INTERMEDIATE_COMMIT) { + sleep(1.millis) + } + try { + serviceHub.withEntityManager { + serviceHub.withEntityManager { + persist(entityWithIdTwo) + } + persist(anotherEntityWithIdOne) + } + } catch (e: PersistenceException) { + logger.info("Caught the exception!") + } + sleep(1.millis) + } + } + + @StartableByRPC + class EntityManagerThatSavesAnEntityUsingInternalEntityManagerAndThrowsFromOuterAndCatchesInsideOuterEntityManagerAfterSavingFlow( + private val commitStatus: CommitStatus + ) : + FlowLogic() { + + @Suspendable + override fun call() { + serviceHub.withEntityManager { + persist(entityWithIdOne) + } + if (commitStatus == CommitStatus.INTERMEDIATE_COMMIT) { + sleep(1.millis) + } + serviceHub.withEntityManager { + serviceHub.withEntityManager { + persist(entityWithIdTwo) + } + try { + persist(anotherEntityWithIdOne) + flush() + } catch (e: PersistenceException) { + logger.info("Caught the exception!") + } + } + sleep(1.millis) + } + } +} \ No newline at end of file diff --git a/node/src/integration-test/kotlin/net/corda/node/flows/FlowEntityManagerStatementTest.kt b/node/src/integration-test/kotlin/net/corda/node/flows/FlowEntityManagerStatementTest.kt new file mode 100644 index 0000000000..a6e2c8946d --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/node/flows/FlowEntityManagerStatementTest.kt @@ -0,0 +1,309 @@ +package net.corda.node.flows + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.StartableByRPC +import net.corda.core.messaging.startFlow +import net.corda.core.utilities.getOrThrow +import net.corda.core.utilities.millis +import net.corda.core.utilities.seconds +import net.corda.node.services.statemachine.StaffedFlowHospital +import net.corda.testing.core.ALICE_NAME +import net.corda.testing.driver.DriverParameters +import net.corda.testing.driver.driver +import org.hibernate.exception.ConstraintViolationException +import org.junit.Test +import javax.persistence.PersistenceException +import kotlin.test.assertEquals + +class FlowEntityManagerStatementTest : AbstractFlowEntityManagerTest() { + + @Test(timeout = 300_000) + fun `data can be saved by a sql statement using entity manager`() { + var counter = 0 + StaffedFlowHospital.onFlowDischarged.add { _, _ -> ++counter } + driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { + val alice = startNode(providedName = ALICE_NAME).getOrThrow() + + alice.rpc.startFlow(::EntityManagerSqlFlow).returnValue.getOrThrow(20.seconds) + assertEquals(0, counter) + val entities = alice.rpc.startFlow(::GetCustomEntities).returnValue.getOrThrow() + assertEquals(1, entities.size) + } + } + + @Test(timeout = 300_000) + fun `constraint violation caused by a sql statement should save no data`() { + driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { + val alice = startNode(providedName = ALICE_NAME).getOrThrow() + alice.rpc.expectFlowFailureAndAssertCreatedEntities( + flow = ::EntityManagerErrorFromSqlFlow, + commitStatus = CommitStatus.NO_INTERMEDIATE_COMMIT, + numberOfDischarges = 3, + numberOfExpectedEntities = 0 + ) + alice.rpc.expectFlowFailureAndAssertCreatedEntities( + flow = ::EntityManagerErrorFromSqlFlow, + commitStatus = CommitStatus.INTERMEDIATE_COMMIT, + numberOfDischarges = 3, + numberOfExpectedEntities = 1 + ) + } + } + + @Test(timeout = 300_000) + fun `constraint violation caused by a sql statement that is caught inside an entity manager block saves none of the data inside of it`() { + driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { + val alice = startNode(providedName = ALICE_NAME).getOrThrow() + // 1 entity saved from the first entity manager block that does not get rolled back + // even if there is no intermediate commit to the database + alice.rpc.expectFlowSuccessAndAssertCreatedEntities( + flow = ::EntityManagerCatchErrorFromSqlInsideTheEntityManagerFlow, + commitStatus = CommitStatus.NO_INTERMEDIATE_COMMIT, + numberOfDischarges = 0, + numberOfExpectedEntities = 1 + ) + alice.rpc.expectFlowSuccessAndAssertCreatedEntities( + flow = ::EntityManagerCatchErrorFromSqlInsideTheEntityManagerFlow, + commitStatus = CommitStatus.INTERMEDIATE_COMMIT, + numberOfDischarges = 0, + numberOfExpectedEntities = 1 + ) + } + } + + @Test(timeout = 300_000) + fun `constraint violation caused by a sql statement that is caught outside an entity manager block saves none of the data inside of it`() { + driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { + val alice = startNode(providedName = ALICE_NAME).getOrThrow() + // 1 entity saved from the first entity manager block that does not get rolled back + // even if there is no intermediate commit to the database + alice.rpc.expectFlowSuccessAndAssertCreatedEntities( + flow = ::EntityManagerCatchErrorFromSqlOutsideTheEntityManagerFlow, + commitStatus = CommitStatus.NO_INTERMEDIATE_COMMIT, + numberOfDischarges = 0, + numberOfExpectedEntities = 1 + ) + alice.rpc.expectFlowSuccessAndAssertCreatedEntities( + flow = ::EntityManagerCatchErrorFromSqlOutsideTheEntityManagerFlow, + commitStatus = CommitStatus.INTERMEDIATE_COMMIT, + numberOfDischarges = 0, + numberOfExpectedEntities = 1 + ) + } + } + + @Test(timeout = 300_000) + fun `constraint violation caused by a sql statement that is caught inside an entity manager and more data is saved afterwards inside the same entity manager should not save the extra data`() { + driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { + + val alice = startNode(providedName = ALICE_NAME).getOrThrow() + alice.rpc.expectFlowSuccessAndAssertCreatedEntities( + flow = ::EntityManagerCatchErrorFromSqlAndSaveMoreEntitiesInTheSameEntityManagerFlow, + commitStatus = CommitStatus.NO_INTERMEDIATE_COMMIT, + numberOfDischarges = 0, + numberOfExpectedEntities = 1 + ) + alice.rpc.expectFlowSuccessAndAssertCreatedEntities( + flow = ::EntityManagerCatchErrorFromSqlAndSaveMoreEntitiesInTheSameEntityManagerFlow, + commitStatus = CommitStatus.INTERMEDIATE_COMMIT, + numberOfDischarges = 0, + numberOfExpectedEntities = 1 + ) + } + } + + @Test(timeout = 300_000) + fun `constraint violation caused by a sql statement that is caught inside an entity manager and more data is saved afterwards inside a new entity manager should save the extra data`() { + driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { + + val alice = startNode(providedName = ALICE_NAME).getOrThrow() + alice.rpc.expectFlowSuccessAndAssertCreatedEntities( + flow = ::EntityManagerCatchErrorFromSqlAndSaveMoreEntitiesInNewEntityManagerFlow, + commitStatus = CommitStatus.NO_INTERMEDIATE_COMMIT, + numberOfDischarges = 0, + numberOfExpectedEntities = 2 + ) + alice.rpc.expectFlowSuccessAndAssertCreatedEntities( + flow = ::EntityManagerCatchErrorFromSqlAndSaveMoreEntitiesInNewEntityManagerFlow, + commitStatus = CommitStatus.INTERMEDIATE_COMMIT, + numberOfDischarges = 0, + numberOfExpectedEntities = 2 + ) + } + } + + @StartableByRPC + class EntityManagerSqlFlow : FlowLogic() { + + @Suspendable + override fun call() { + serviceHub.withEntityManager { + createNativeQuery("INSERT INTO $TABLE_NAME VALUES (:id, :name, :quote)") + .setParameter("id", anotherEntityWithIdOne.id) + .setParameter("name", anotherEntityWithIdOne.name) + .setParameter("quote", anotherEntityWithIdOne.name) + .executeUpdate() + } + sleep(1.millis) + } + } + + @StartableByRPC + class EntityManagerErrorFromSqlFlow(private val commitStatus: CommitStatus) : + FlowLogic() { + + @Suspendable + override fun call() { + serviceHub.withEntityManager { + persist(entityWithIdOne) + } + if (commitStatus == CommitStatus.INTERMEDIATE_COMMIT) { + sleep(1.millis) + } + serviceHub.withEntityManager { + createNativeQuery("INSERT INTO $TABLE_NAME VALUES (:id, :name, :quote)") + .setParameter("id", anotherEntityWithIdOne.id) + .setParameter("name", anotherEntityWithIdOne.name) + .setParameter("quote", anotherEntityWithIdOne.name) + .executeUpdate() + } + sleep(1.millis) + } + } + + @StartableByRPC + class EntityManagerCatchErrorFromSqlInsideTheEntityManagerFlow(private val commitStatus: CommitStatus) : + FlowLogic() { + + @Suspendable + override fun call() { + serviceHub.withEntityManager { + persist(entityWithIdOne) + } + if (commitStatus == CommitStatus.INTERMEDIATE_COMMIT) { + sleep(1.millis) + } + serviceHub.withEntityManager { + try { + createNativeQuery("INSERT INTO $TABLE_NAME VALUES (:id, :name, :quote)") + .setParameter("id", anotherEntityWithIdOne.id) + .setParameter("name", anotherEntityWithIdOne.name) + .setParameter("quote", anotherEntityWithIdOne.name) + .executeUpdate() + } catch (e: PersistenceException) { + logger.info("Caught the exception!") + } + } + sleep(1.millis) + } + } + + @StartableByRPC + class EntityManagerCatchErrorFromSqlOutsideTheEntityManagerFlow(private val commitStatus: CommitStatus) : + FlowLogic() { + + @Suspendable + override fun call() { + serviceHub.withEntityManager { + persist(entityWithIdOne) + } + if (commitStatus == CommitStatus.INTERMEDIATE_COMMIT) { + sleep(1.millis) + } + try { + serviceHub.withEntityManager { + createNativeQuery("INSERT INTO $TABLE_NAME VALUES (:id, :name, :quote)") + .setParameter("id", anotherEntityWithIdOne.id) + .setParameter("name", anotherEntityWithIdOne.name) + .setParameter("quote", anotherEntityWithIdOne.name) + .executeUpdate() + } + } catch (e: PersistenceException) { + logger.info("Caught the exception!") + } + sleep(1.millis) + } + } + + @StartableByRPC + class EntityManagerCatchErrorFromSqlAndSaveMoreEntitiesInTheSameEntityManagerFlow(private val commitStatus: CommitStatus) : + FlowLogic() { + + @Suspendable + override fun call() { + serviceHub.withEntityManager { + persist(entityWithIdOne) + } + if (commitStatus == CommitStatus.INTERMEDIATE_COMMIT) { + sleep(1.millis) + } + serviceHub.withEntityManager { + try { + createNativeQuery("INSERT INTO $TABLE_NAME VALUES (:id, :name, :quote)") + .setParameter("id", anotherEntityWithIdOne.id) + .setParameter("name", anotherEntityWithIdOne.name) + .setParameter("quote", anotherEntityWithIdOne.name) + .executeUpdate() + } catch (e: PersistenceException) { + logger.info("Caught the exception!") + } + // These entities are not saved since the transaction is marked for rollback + try { + createNativeQuery("INSERT INTO $TABLE_NAME VALUES (:id, :name, :quote)") + .setParameter("id", entityWithIdTwo.id) + .setParameter("name", entityWithIdTwo.name) + .setParameter("quote", entityWithIdTwo.name) + .executeUpdate() + } catch (e: PersistenceException) { + if (e.cause is ConstraintViolationException) { + throw e + } else { + logger.info( + """ + Caught exception from second sql statement inside the same broken entity manager + This happens if the database has thrown an exception due to rolling back the db transaction + """.trimIndent(), e + ) + } + } + } + sleep(1.millis) + } + } + + @StartableByRPC + class EntityManagerCatchErrorFromSqlAndSaveMoreEntitiesInNewEntityManagerFlow(private val commitStatus: CommitStatus) : + FlowLogic() { + + @Suspendable + override fun call() { + serviceHub.withEntityManager { + persist(entityWithIdOne) + } + if (commitStatus == CommitStatus.INTERMEDIATE_COMMIT) { + sleep(1.millis) + } + try { + serviceHub.withEntityManager { + createNativeQuery("INSERT INTO $TABLE_NAME VALUES (:id, :name, :quote)") + .setParameter("id", anotherEntityWithIdOne.id) + .setParameter("name", anotherEntityWithIdOne.name) + .setParameter("quote", anotherEntityWithIdOne.name) + .executeUpdate() + + } + } catch (e: PersistenceException) { + logger.info("Caught the exception!") + } + serviceHub.withEntityManager { + val query = createNativeQuery("INSERT INTO $TABLE_NAME VALUES (:id, :name, :quote)") + .setParameter("id", entityWithIdTwo.id) + .setParameter("name", entityWithIdTwo.name) + .setParameter("quote", entityWithIdTwo.name) + query.executeUpdate() + } + sleep(1.millis) + } + } +} \ No newline at end of file diff --git a/node/src/integration-test/kotlin/net/corda/node/flows/FlowEntityManagerTest.kt b/node/src/integration-test/kotlin/net/corda/node/flows/FlowEntityManagerTest.kt new file mode 100644 index 0000000000..f8c68f0cb6 --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/node/flows/FlowEntityManagerTest.kt @@ -0,0 +1,763 @@ +package net.corda.node.flows + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.crypto.SecureHash +import net.corda.core.flows.CollectSignaturesFlow +import net.corda.core.flows.FinalityFlow +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.FlowSession +import net.corda.core.flows.InitiatedBy +import net.corda.core.flows.InitiatingFlow +import net.corda.core.flows.ReceiveFinalityFlow +import net.corda.core.flows.SignTransactionFlow +import net.corda.core.flows.StartableByRPC +import net.corda.core.identity.Party +import net.corda.core.messaging.startFlow +import net.corda.core.node.AppServiceHub +import net.corda.core.node.services.CordaService +import net.corda.core.serialization.SingletonSerializeAsToken +import net.corda.core.transactions.SignedTransaction +import net.corda.core.transactions.TransactionBuilder +import net.corda.core.utilities.getOrThrow +import net.corda.core.utilities.millis +import net.corda.core.utilities.seconds +import net.corda.node.services.statemachine.StaffedFlowHospital +import net.corda.testing.contracts.DummyState +import net.corda.testing.core.ALICE_NAME +import net.corda.testing.core.BOB_NAME +import net.corda.testing.core.DummyCommandData +import net.corda.testing.core.singleIdentity +import net.corda.testing.driver.DriverParameters +import net.corda.testing.driver.driver +import org.hibernate.exception.ConstraintViolationException +import org.junit.Before +import org.junit.Test +import java.sql.Connection +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import java.util.concurrent.Semaphore +import javax.persistence.PersistenceException +import kotlin.test.assertEquals + +class FlowEntityManagerTest : AbstractFlowEntityManagerTest() { + + @Before + override fun before() { + MyService.includeRawUpdates = false + super.before() + } + + @Test(timeout = 300_000) + fun `entities can be saved using entity manager without a flush`() { + var counter = 0 + StaffedFlowHospital.onFlowDischarged.add { _, _ -> ++counter } + + driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { + + val alice = startNode(providedName = ALICE_NAME).getOrThrow() + alice.rpc.startFlow(::EntityManagerSaveEntitiesWithoutAFlushFlow) + .returnValue.getOrThrow(30.seconds) + assertEquals(0, counter) + val entities = alice.rpc.startFlow(::GetCustomEntities).returnValue.getOrThrow() + assertEquals(3, entities.size) + } + } + + @Test(timeout = 300_000) + fun `entities can be saved using entity manager with a flush`() { + var counter = 0 + StaffedFlowHospital.onFlowDischarged.add { _, _ -> ++counter } + driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { + + val alice = startNode(providedName = ALICE_NAME).getOrThrow() + + alice.rpc.startFlow(::EntityManagerSaveEntitiesWithAFlushFlow) + .returnValue.getOrThrow(30.seconds) + assertEquals(0, counter) + val entities = alice.rpc.startFlow(::GetCustomEntities).returnValue.getOrThrow() + assertEquals(3, entities.size) + } + } + + @Test(timeout = 300_000) + fun `entities saved inside an entity manager are only committed when a flow suspends`() { + var counter = 0 + StaffedFlowHospital.onFlowDischarged.add { _, _ -> ++counter } + driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { + + val alice = startNode(providedName = ALICE_NAME).getOrThrow() + + var beforeCommitEntities: List? = null + EntityManagerSaveEntitiesWithoutAFlushFlow.beforeCommitHook = { + beforeCommitEntities = it + } + var afterCommitEntities: List? = null + EntityManagerSaveEntitiesWithoutAFlushFlow.afterCommitHook = { + afterCommitEntities = it + } + + alice.rpc.startFlow(::EntityManagerSaveEntitiesWithoutAFlushFlow) + .returnValue.getOrThrow(30.seconds) + assertEquals(0, counter) + val entities = alice.rpc.startFlow(::GetCustomEntities).returnValue.getOrThrow() + assertEquals(3, entities.size) + assertEquals(0, beforeCommitEntities!!.size) + assertEquals(3, afterCommitEntities!!.size) + } + } + + @Test(timeout = 300_000) + fun `constraint violation without a flush breaks`() { + driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { + + val alice = startNode(providedName = ALICE_NAME).getOrThrow() + alice.rpc.expectFlowFailureAndAssertCreatedEntities( + flow = ::EntityManagerErrorWithoutAFlushFlow, + commitStatus = CommitStatus.NO_INTERMEDIATE_COMMIT, + numberOfDischarges = 3, + numberOfExpectedEntities = 0 + ) + alice.rpc.expectFlowFailureAndAssertCreatedEntities( + flow = ::EntityManagerErrorWithoutAFlushFlow, + commitStatus = CommitStatus.INTERMEDIATE_COMMIT, + numberOfDischarges = 3, + numberOfExpectedEntities = 1 + ) + } + } + + @Test(timeout = 300_000) + fun `constraint violation with a flush breaks`() { + driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { + + val alice = startNode(providedName = ALICE_NAME).getOrThrow() + alice.rpc.expectFlowFailureAndAssertCreatedEntities( + flow = ::EntityManagerErrorWithAFlushFlow, + commitStatus = CommitStatus.NO_INTERMEDIATE_COMMIT, + numberOfDischarges = 3, + numberOfExpectedEntities = 0 + ) + alice.rpc.expectFlowFailureAndAssertCreatedEntities( + flow = ::EntityManagerErrorWithAFlushFlow, + commitStatus = CommitStatus.INTERMEDIATE_COMMIT, + numberOfDischarges = 3, + numberOfExpectedEntities = 1 + ) + } + } + + @Test(timeout = 300_000) + fun `constraint violation with a flush that is caught inside an entity manager block saves none of the data inside of it`() { + driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { + + val alice = startNode(providedName = ALICE_NAME).getOrThrow() + // 1 entity saved from the first entity manager block that does not get rolled back + // even if there is no intermediate commit to the database + alice.rpc.expectFlowSuccessAndAssertCreatedEntities( + flow = ::EntityManagerWithAFlushCatchErrorInsideTheEntityManagerFlow, + commitStatus = CommitStatus.NO_INTERMEDIATE_COMMIT, + numberOfDischarges = 0, + numberOfExpectedEntities = 1 + ) + alice.rpc.expectFlowSuccessAndAssertCreatedEntities( + flow = ::EntityManagerWithAFlushCatchErrorInsideTheEntityManagerFlow, + commitStatus = CommitStatus.INTERMEDIATE_COMMIT, + numberOfDischarges = 0, + numberOfExpectedEntities = 1 + ) + } + } + + @Test(timeout = 300_000) + fun `constraint violation with a flush that is caught outside the entity manager block saves none of the data inside of it`() { + driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { + + val alice = startNode(providedName = ALICE_NAME).getOrThrow() + // 1 entity saved from the first entity manager block that does not get rolled back + // even if there is no intermediate commit to the database + alice.rpc.expectFlowSuccessAndAssertCreatedEntities( + flow = ::EntityManagerWithAFlushCatchErrorOutsideTheEntityManagerFlow, + commitStatus = CommitStatus.NO_INTERMEDIATE_COMMIT, + numberOfDischarges = 0, + numberOfExpectedEntities = 1 + ) + alice.rpc.expectFlowSuccessAndAssertCreatedEntities( + flow = ::EntityManagerWithAFlushCatchErrorOutsideTheEntityManagerFlow, + commitStatus = CommitStatus.INTERMEDIATE_COMMIT, + numberOfDischarges = 0, + numberOfExpectedEntities = 1 + ) + } + } + + @Test(timeout = 300_000) + fun `constraint violation within a single entity manager block throws an exception and saves no data`() { + var dischargeCounter = 0 + StaffedFlowHospital.onFlowDischarged.add { _, _ -> ++dischargeCounter } + val lock = Semaphore(0) + StaffedFlowHospital.onFlowKeptForOvernightObservation.add { _, _ -> lock.release() } + driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { + + val alice = startNode(providedName = ALICE_NAME).getOrThrow() + alice.rpc.startFlow(::EntityManagerErrorInsideASingleEntityManagerFlow) + lock.acquire() + // Goes straight to observation due to throwing [EntityExistsException] + assertEquals(0, dischargeCounter) + val entities = alice.rpc.startFlow(::GetCustomEntities).returnValue.getOrThrow() + assertEquals(0, entities.size) + } + } + + @Test(timeout = 300_000) + fun `constraint violation on a single entity when saving multiple entities throws an exception and does not save any data within the entity manager block`() { + driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { + + val alice = startNode(providedName = ALICE_NAME).getOrThrow() + alice.rpc.expectFlowFailureAndAssertCreatedEntities( + flow = ::EntityManagerSavingMultipleEntitiesWithASingleErrorFlow, + commitStatus = CommitStatus.NO_INTERMEDIATE_COMMIT, + numberOfDischarges = 3, + numberOfExpectedEntities = 0 + ) + alice.rpc.expectFlowFailureAndAssertCreatedEntities( + flow = ::EntityManagerSavingMultipleEntitiesWithASingleErrorFlow, + commitStatus = CommitStatus.INTERMEDIATE_COMMIT, + numberOfDischarges = 3, + numberOfExpectedEntities = 1 + ) + } + } + + @Test(timeout = 300_000) + fun `constraint violation on a single entity when saving multiple entities and catching the error does not save any data within the entity manager block`() { + driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { + + val alice = startNode(providedName = ALICE_NAME).getOrThrow() + // 1 entity saved from the first entity manager block that does not get rolled back + // even if there is no intermediate commit to the database + alice.rpc.expectFlowSuccessAndAssertCreatedEntities( + flow = ::EntityManagerSavingMultipleEntitiesWithASingleCaughtErrorFlow, + commitStatus = CommitStatus.NO_INTERMEDIATE_COMMIT, + numberOfDischarges = 0, + numberOfExpectedEntities = 1 + ) + alice.rpc.expectFlowSuccessAndAssertCreatedEntities( + flow = ::EntityManagerSavingMultipleEntitiesWithASingleCaughtErrorFlow, + commitStatus = CommitStatus.INTERMEDIATE_COMMIT, + numberOfDischarges = 0, + numberOfExpectedEntities = 1 + ) + } + } + + @Test(timeout = 300_000) + fun `constraint violation that is caught inside an entity manager and more data is saved afterwards inside a new entity manager should save the extra data`() { + driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { + + val alice = startNode(providedName = ALICE_NAME).getOrThrow() + alice.rpc.expectFlowSuccessAndAssertCreatedEntities( + flow = ::EntityManagerCatchErrorAndSaveMoreEntitiesInANewEntityManager, + commitStatus = CommitStatus.NO_INTERMEDIATE_COMMIT, + numberOfDischarges = 0, + numberOfExpectedEntities = 3 + ) + alice.rpc.expectFlowSuccessAndAssertCreatedEntities( + flow = ::EntityManagerCatchErrorAndSaveMoreEntitiesInANewEntityManager, + commitStatus = CommitStatus.INTERMEDIATE_COMMIT, + numberOfDischarges = 0, + numberOfExpectedEntities = 3 + ) + } + } + + @Test(timeout = 300_000) + fun `constraint violation that is caught inside an entity manager and more data is saved afterwards inside the same entity manager should not save the extra data`() { + driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { + + val alice = startNode(providedName = ALICE_NAME).getOrThrow() + alice.rpc.expectFlowSuccessAndAssertCreatedEntities( + flow = ::EntityManagerCatchErrorAndSaveMoreEntitiesInTheSameEntityManager, + commitStatus = CommitStatus.NO_INTERMEDIATE_COMMIT, + numberOfDischarges = 0, + numberOfExpectedEntities = 1 + ) + alice.rpc.expectFlowSuccessAndAssertCreatedEntities( + flow = ::EntityManagerCatchErrorAndSaveMoreEntitiesInTheSameEntityManager, + commitStatus = CommitStatus.INTERMEDIATE_COMMIT, + numberOfDischarges = 0, + numberOfExpectedEntities = 1 + ) + } + } + + @Test(timeout = 300_000) + fun `constraint violation that is caught outside an entity manager and more data is saved afterwards inside a new entity manager should save the extra data`() { + driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { + + val alice = startNode(providedName = ALICE_NAME).getOrThrow() + alice.rpc.expectFlowSuccessAndAssertCreatedEntities( + flow = ::EntityManagerCatchErrorOutsideTheEntityManagerAndSaveMoreEntitiesInANewEntityManager, + commitStatus = CommitStatus.NO_INTERMEDIATE_COMMIT, + numberOfDischarges = 0, + numberOfExpectedEntities = 3 + ) + alice.rpc.expectFlowSuccessAndAssertCreatedEntities( + flow = ::EntityManagerCatchErrorOutsideTheEntityManagerAndSaveMoreEntitiesInANewEntityManager, + commitStatus = CommitStatus.INTERMEDIATE_COMMIT, + numberOfDischarges = 0, + numberOfExpectedEntities = 3 + ) + } + } + + @Test(timeout = 300_000) + fun `constraint violation that is caught inside an entity manager should allow a flow to continue processing as normal`() { + var counter = 0 + StaffedFlowHospital.onFlowDischarged.add { _, _ -> ++counter } + driver(DriverParameters(startNodesInProcess = true)) { + + val alice = startNode(providedName = ALICE_NAME).getOrThrow() + val bob = startNode(providedName = BOB_NAME).getOrThrow() + + val txId = + alice.rpc.startFlow(::EntityManagerWithFlushCatchAndInteractWithOtherPartyFlow, bob.nodeInfo.singleIdentity()) + .returnValue.getOrThrow(20.seconds) + assertEquals(0, counter) + val txFromVault = alice.rpc.stateMachineRecordedTransactionMappingSnapshot().firstOrNull()?.transactionId + assertEquals(txId, txFromVault) + val entity = alice.rpc.startFlow(::GetCustomEntities).returnValue.getOrThrow().single() + assertEquals(entityWithIdOne, entity) + } + } + + @Test(timeout = 300_000) + fun `data saved from an entity manager vault update should be visible within an entity manager block inside the same database transaction`() { + MyService.includeRawUpdates = true + MyService.insertionType = MyService.InsertionType.ENTITY_MANAGER + var counter = 0 + StaffedFlowHospital.onFlowDischarged.add { _, _ -> ++counter } + driver(DriverParameters(startNodesInProcess = true)) { + + val alice = startNode(providedName = ALICE_NAME).getOrThrow() + + val entities = + alice.rpc.startFlow(::EntityManagerWithinTheSameDatabaseTransactionFlow).returnValue.getOrThrow(20.seconds) + assertEquals(3, entities.size) + assertEquals(0, counter) + } + } + + @Test(timeout = 300_000) + fun `data saved from a jdbc connection vault update should be visible within an entity manager block inside the same database transaction`() { + MyService.includeRawUpdates = true + MyService.insertionType = MyService.InsertionType.CONNECTION + var counter = 0 + StaffedFlowHospital.onFlowDischarged.add { _, _ -> ++counter } + driver(DriverParameters(startNodesInProcess = true)) { + + val alice = startNode(providedName = ALICE_NAME).getOrThrow() + + val entities = + alice.rpc.startFlow(::EntityManagerWithinTheSameDatabaseTransactionFlow).returnValue.getOrThrow(20.seconds) + assertEquals(3, entities.size) + assertEquals(0, counter) + } + } + + @StartableByRPC + class EntityManagerSaveEntitiesWithoutAFlushFlow : FlowLogic() { + + companion object { + var beforeCommitHook: ((entities: List) -> Unit)? = null + var afterCommitHook: ((entities: List) -> Unit)? = null + } + + @Suspendable + override fun call() { + serviceHub.withEntityManager { + persist(entityWithIdOne) + persist(entityWithIdTwo) + persist(entityWithIdThree) + } + beforeCommitHook?.invoke(serviceHub.cordaService(MyService::class.java).getEntities()) + sleep(1.millis) + afterCommitHook?.invoke(serviceHub.cordaService(MyService::class.java).getEntities()) + } + } + + @StartableByRPC + class EntityManagerSaveEntitiesWithAFlushFlow : FlowLogic() { + + @Suspendable + override fun call() { + serviceHub.withEntityManager { + persist(entityWithIdOne) + persist(entityWithIdTwo) + persist(entityWithIdThree) + flush() + } + sleep(1.millis) + } + } + + @StartableByRPC + class EntityManagerErrorWithoutAFlushFlow(private val commitStatus: CommitStatus) : FlowLogic() { + + @Suspendable + override fun call() { + serviceHub.withEntityManager { + persist(entityWithIdOne) + } + if (commitStatus == CommitStatus.INTERMEDIATE_COMMIT) { + sleep(1.millis) + } + serviceHub.withEntityManager { + persist(anotherEntityWithIdOne) + } + sleep(1.millis) + } + } + + @StartableByRPC + class EntityManagerErrorWithAFlushFlow(private val commitStatus: CommitStatus) : FlowLogic() { + + @Suspendable + override fun call() { + serviceHub.withEntityManager { + persist(entityWithIdOne) + } + if (commitStatus == CommitStatus.INTERMEDIATE_COMMIT) { + sleep(1.millis) + } + serviceHub.withEntityManager { + persist(anotherEntityWithIdOne) + flush() + } + sleep(1.millis) + } + } + + @StartableByRPC + class EntityManagerWithAFlushCatchErrorInsideTheEntityManagerFlow(private val commitStatus: CommitStatus) : FlowLogic() { + + @Suspendable + override fun call() { + serviceHub.withEntityManager { + persist(entityWithIdOne) + } + if (commitStatus == CommitStatus.INTERMEDIATE_COMMIT) { + sleep(1.millis) + } + serviceHub.withEntityManager { + persist(anotherEntityWithIdOne) + try { + flush() + } catch (e: PersistenceException) { + logger.info("Caught the exception!") + } + } + sleep(1.millis) + } + } + + @StartableByRPC + class EntityManagerWithAFlushCatchErrorOutsideTheEntityManagerFlow(private val commitStatus: CommitStatus) : FlowLogic() { + + @Suspendable + override fun call() { + serviceHub.withEntityManager { + persist(entityWithIdOne) + } + if (commitStatus == CommitStatus.INTERMEDIATE_COMMIT) { + sleep(1.millis) + } + try { + serviceHub.withEntityManager { + persist(anotherEntityWithIdOne) + flush() + } + } catch (e: PersistenceException) { + logger.info("Caught the exception!") + } + sleep(1.millis) + } + } + + @StartableByRPC + class EntityManagerErrorInsideASingleEntityManagerFlow : FlowLogic() { + + @Suspendable + override fun call() { + serviceHub.withEntityManager { + persist(entityWithIdOne) + persist(anotherEntityWithIdOne) + } + sleep(1.millis) + } + } + + @StartableByRPC + class EntityManagerSavingMultipleEntitiesWithASingleErrorFlow(private val commitStatus: CommitStatus) : FlowLogic() { + + @Suspendable + override fun call() { + serviceHub.withEntityManager { + persist(entityWithIdOne) + } + if (commitStatus == CommitStatus.INTERMEDIATE_COMMIT) { + sleep(1.millis) + } + serviceHub.withEntityManager { + persist(anotherEntityWithIdOne) + persist(entityWithIdTwo) + persist(entityWithIdThree) + } + sleep(1.millis) + } + } + + @StartableByRPC + class EntityManagerSavingMultipleEntitiesWithASingleCaughtErrorFlow(private val commitStatus: CommitStatus) : FlowLogic() { + + @Suspendable + override fun call() { + serviceHub.withEntityManager { + persist(entityWithIdOne) + } + if (commitStatus == CommitStatus.INTERMEDIATE_COMMIT) { + sleep(1.millis) + } + serviceHub.withEntityManager { + persist(anotherEntityWithIdOne) + persist(entityWithIdTwo) + persist(entityWithIdThree) + try { + flush() + } catch (e: PersistenceException) { + logger.info("Caught the exception!") + } + } + sleep(1.millis) + } + } + + @StartableByRPC + class EntityManagerCatchErrorAndSaveMoreEntitiesInANewEntityManager(private val commitStatus: CommitStatus) : FlowLogic() { + + @Suspendable + override fun call() { + serviceHub.withEntityManager { + persist(entityWithIdOne) + } + if (commitStatus == CommitStatus.INTERMEDIATE_COMMIT) { + sleep(1.millis) + } + serviceHub.withEntityManager { + persist(anotherEntityWithIdOne) + try { + flush() + } catch (e: PersistenceException) { + logger.info("Caught the exception!") + } + } + if (commitStatus == CommitStatus.INTERMEDIATE_COMMIT) { + sleep(1.millis) + } + serviceHub.withEntityManager { + persist(entityWithIdTwo) + persist(entityWithIdThree) + } + sleep(1.millis) + } + } + + @StartableByRPC + class EntityManagerCatchErrorAndSaveMoreEntitiesInTheSameEntityManager(private val commitStatus: CommitStatus) : FlowLogic() { + + @Suspendable + override fun call() { + serviceHub.withEntityManager { + persist(entityWithIdOne) + } + if (commitStatus == CommitStatus.INTERMEDIATE_COMMIT) { + sleep(1.millis) + } + serviceHub.withEntityManager { + persist(anotherEntityWithIdOne) + try { + flush() + } catch (e: PersistenceException) { + logger.info("Caught the exception!") + } + // These entities are not saved since the transaction is marked for rollback + try { + persist(entityWithIdTwo) + persist(entityWithIdThree) + } catch (e: PersistenceException) { + if (e.cause is ConstraintViolationException) { + throw e + } else { + logger.info( + """ + Caught exception from second set of persists inside the same broken entity manager + This happens if the database has thrown an exception due to rolling back the db transaction + """.trimIndent(), e + ) + } + } + } + sleep(1.millis) + } + } + + @StartableByRPC + class EntityManagerCatchErrorOutsideTheEntityManagerAndSaveMoreEntitiesInANewEntityManager(private val commitStatus: CommitStatus) : + FlowLogic() { + + @Suspendable + override fun call() { + serviceHub.withEntityManager { + persist(entityWithIdOne) + } + if (commitStatus == CommitStatus.INTERMEDIATE_COMMIT) { + sleep(1.millis) + } + try { + serviceHub.withEntityManager { + persist(anotherEntityWithIdOne) + } + } catch (e: PersistenceException) { + logger.info("Caught the exception!") + } + if (commitStatus == CommitStatus.INTERMEDIATE_COMMIT) { + sleep(1.millis) + } + serviceHub.withEntityManager { + persist(entityWithIdTwo) + persist(entityWithIdThree) + } + sleep(1.millis) + } + } + + @StartableByRPC + class EntityManagerWithFlushCatchAndInteractWithOtherPartyFlow(private val party: Party) : FlowLogic() { + + @Suspendable + override fun call(): SecureHash { + serviceHub.withEntityManager { + persist(entityWithIdOne) + } + serviceHub.withEntityManager { + persist(anotherEntityWithIdOne) + try { + flush() + } catch (e: PersistenceException) { + logger.info("Caught the exception!") + } + } + return subFlow(CreateATransactionFlow(party)) + } + } + + @InitiatingFlow + class CreateATransactionFlow(val party: Party) : FlowLogic() { + @Suspendable + override fun call(): SecureHash { + val session = initiateFlow(party) + val tx = TransactionBuilder(serviceHub.networkMapCache.notaryIdentities.first()).apply { + addOutputState(DummyState(participants = listOf(ourIdentity, party))) + addCommand(DummyCommandData, ourIdentity.owningKey, party.owningKey) + } + val stx = serviceHub.signInitialTransaction(tx) + val ftx = subFlow(CollectSignaturesFlow(stx, listOf(session))) + return subFlow(FinalityFlow(ftx, session)).id + } + } + + @InitiatedBy(CreateATransactionFlow::class) + class CreateATransactionResponder(val session: FlowSession) : FlowLogic() { + @Suspendable + override fun call() { + val stx = subFlow(object : SignTransactionFlow(session) { + override fun checkTransaction(stx: SignedTransaction) { + } + }) + subFlow(ReceiveFinalityFlow(session, stx.id)) + } + } + + @StartableByRPC + class EntityManagerWithinTheSameDatabaseTransactionFlow : FlowLogic>() { + + @Suspendable + override fun call(): List { + val tx = TransactionBuilder(serviceHub.networkMapCache.notaryIdentities.first()).apply { + addOutputState(DummyState(participants = listOf(ourIdentity))) + addCommand(DummyCommandData, ourIdentity.owningKey) + } + val stx = serviceHub.signInitialTransaction(tx) + serviceHub.recordTransactions(stx) + return serviceHub.withEntityManager { + val criteria = criteriaBuilder.createQuery(CustomTableEntity::class.java) + criteria.select(criteria.from(CustomTableEntity::class.java)) + createQuery(criteria).resultList + } + } + } + + @CordaService + class MyService(private val services: AppServiceHub) : SingletonSerializeAsToken() { + + companion object { + var includeRawUpdates = false + var insertionType = InsertionType.ENTITY_MANAGER + } + + enum class InsertionType { ENTITY_MANAGER, CONNECTION } + + val executors: ExecutorService = Executors.newFixedThreadPool(1) + + init { + if (includeRawUpdates) { + services.register { + services.vaultService.rawUpdates.subscribe { + if (insertionType == InsertionType.ENTITY_MANAGER) { + services.withEntityManager { + persist(entityWithIdOne) + persist(entityWithIdTwo) + persist(entityWithIdThree) + } + } else { + services.jdbcSession().run { + insert(entityWithIdOne) + insert(entityWithIdTwo) + insert(entityWithIdThree) + } + } + } + } + } + } + + private fun Connection.insert(entity: CustomTableEntity) { + prepareStatement("INSERT INTO $TABLE_NAME VALUES (?, ?, ?)").apply { + setInt(1, entity.id) + setString(2, entity.name) + setString(3, entity.quote) + }.executeUpdate() + } + + fun getEntities(): List { + return executors.submit> { + services.database.transaction { + session.run { + val criteria = criteriaBuilder.createQuery(CustomTableEntity::class.java) + criteria.select(criteria.from(CustomTableEntity::class.java)) + createQuery(criteria).resultList + } + } + }.get() + } + } +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index a89dd09763..462d310c78 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -159,7 +159,9 @@ import net.corda.nodeapi.internal.persistence.CordaTransactionSupportImpl import net.corda.nodeapi.internal.persistence.CouldNotCreateDataSourceException import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.nodeapi.internal.persistence.DatabaseIncompatibleException +import net.corda.nodeapi.internal.persistence.DatabaseTransaction import net.corda.nodeapi.internal.persistence.OutstandingDatabaseChangesException +import net.corda.nodeapi.internal.persistence.RestrictedEntityManager import net.corda.nodeapi.internal.persistence.SchemaMigration import net.corda.tools.shell.InteractiveShell import org.apache.activemq.artemis.utils.ReusableLatch @@ -174,6 +176,7 @@ import java.security.KeyPair import java.security.KeyStoreException import java.security.cert.X509Certificate import java.sql.Connection +import java.sql.Savepoint import java.time.Clock import java.time.Duration import java.time.format.DateTimeParseException @@ -187,6 +190,7 @@ import java.util.concurrent.TimeUnit.MINUTES import java.util.concurrent.TimeUnit.SECONDS import java.util.function.Consumer import javax.persistence.EntityManager +import javax.persistence.PersistenceException import kotlin.collections.ArrayList /** @@ -1163,12 +1167,52 @@ abstract class AbstractNode(val configuration: NodeConfiguration, override fun jdbcSession(): Connection = database.createSession() override fun withEntityManager(block: EntityManager.() -> T): T { - return database.transaction { + return database.transaction(useErrorHandler = false) { session.flush() - block(restrictedEntityManager) + val manager = entityManager + withSavePoint { savepoint -> + // Restrict what entity manager they can use inside the block + try { + block(RestrictedEntityManager(manager)).also { + if (!manager.transaction.rollbackOnly) { + manager.flush() + } else { + connection.rollback(savepoint) + } + } + } catch (e: PersistenceException) { + if (manager.transaction.rollbackOnly) { + connection.rollback(savepoint) + } + throw e + } finally { + manager.close() + } + } } } + private fun DatabaseTransaction.withSavePoint(block: (savepoint: Savepoint) -> T) : T { + val savepoint = connection.setSavepoint() + return try { + block(savepoint) + } finally { + // Release the save point even if we occur an error + if (savepoint.supportsReleasing()) { + connection.releaseSavepoint(savepoint) + } + } + } + + /** + * Not all databases support releasing of savepoints. + * The type of savepoints are referenced by string names since we do not have access to the JDBC drivers + * at compile time. + */ + private fun Savepoint.supportsReleasing(): Boolean { + return this::class.simpleName != "SQLServerSavepoint" && this::class.simpleName != "OracleSavepoint" + } + override fun withEntityManager(block: Consumer) { withEntityManager { block.accept(this) diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/ActionExecutorImpl.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/ActionExecutorImpl.kt index 08cac92f90..53d53c956a 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/ActionExecutorImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/ActionExecutorImpl.kt @@ -223,7 +223,10 @@ class ActionExecutorImpl( @Suspendable private fun executeRollbackTransaction() { - contextTransactionOrNull?.close() + contextTransactionOrNull?.run { + rollback() + close() + } } @Suspendable diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/AppendOnlyPersistentMapTest.kt b/node/src/test/kotlin/net/corda/node/services/persistence/AppendOnlyPersistentMapTest.kt index acfefd9c8e..b596de6757 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/AppendOnlyPersistentMapTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/AppendOnlyPersistentMapTest.kt @@ -5,6 +5,7 @@ import net.corda.core.utilities.loggerFor 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.RolledBackDatabaseSessionException import net.corda.testing.internal.TestingNamedCacheFactory import net.corda.testing.internal.configureDatabase import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties @@ -180,6 +181,8 @@ class AppendOnlyPersistentMapTest(var scenario: Scenario) { } catch (t: PersistenceException) { // This only helps if thrown on commit, otherwise other latches not counted down. assertEquals(t.message, Outcome.SuccessButErrorOnCommit, a.outcome) + } catch (t: RolledBackDatabaseSessionException) { + assertEquals(t.message, Outcome.SuccessButErrorOnCommit, a.outcome) } a.await(a::phase4) b.await(b::phase4) diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultFlowTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultFlowTest.kt index 361d8f296e..993731e15b 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultFlowTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultFlowTest.kt @@ -1,7 +1,12 @@ package net.corda.node.services.vault import co.paralleluniverse.fibers.Suspendable -import net.corda.core.flows.* +import net.corda.core.flows.FinalityFlow +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.FlowSession +import net.corda.core.flows.InitiatedBy +import net.corda.core.flows.InitiatingFlow +import net.corda.core.flows.ReceiveFinalityFlow import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.node.services.queryBy @@ -50,13 +55,11 @@ class VaultFlowTest { @After fun tearDown() { mockNetwork.stopNodes() - StaffedFlowHospital.DatabaseEndocrinologist.customConditions.clear() StaffedFlowHospital.onFlowKeptForOvernightObservation.clear() } @Test(timeout=300_000) fun `Unique column constraint failing causes states to not persist to vaults`() { - StaffedFlowHospital.DatabaseEndocrinologist.customConditions.add( { t: Throwable -> t is javax.persistence.PersistenceException }) partyA.startFlow(Initiator(listOf(partyA.info.singleIdentity(), partyB.info.singleIdentity()))).get() val hospitalLatch = CountDownLatch(1) StaffedFlowHospital.onFlowKeptForOvernightObservation.add { _, _ -> hospitalLatch.countDown() } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt index b46bfaeaae..d4a7d562b3 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt @@ -236,11 +236,11 @@ open class MockServices private constructor( override fun jdbcSession(): Connection = persistence.createSession() override fun withEntityManager(block: EntityManager.() -> T): T { - return block(contextTransaction.restrictedEntityManager) + return block(contextTransaction.entityManager) } override fun withEntityManager(block: Consumer) { - return block.accept(contextTransaction.restrictedEntityManager) + return block.accept(contextTransaction.entityManager) } } } From d35e8ba526d52dfd1ae54ad9d09a83b706930006 Mon Sep 17 00:00:00 2001 From: LankyDan Date: Mon, 29 Jun 2020 15:28:59 +0100 Subject: [PATCH 39/85] CORDA-3722 Add imports in `RestrictedEntityManager` Needed after backporting a change --- .../nodeapi/internal/persistence/RestrictedEntityManager.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/RestrictedEntityManager.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/RestrictedEntityManager.kt index befd492278..1ea4f2c4fd 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/RestrictedEntityManager.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/RestrictedEntityManager.kt @@ -1,6 +1,9 @@ package net.corda.nodeapi.internal.persistence import javax.persistence.EntityManager +import javax.persistence.EntityTransaction +import javax.persistence.LockModeType +import javax.persistence.metamodel.Metamodel /** * A delegate of [EntityManager] which disallows some operations. From bf4d733336ea73e3eedb27419a15ccd5c31b29e0 Mon Sep 17 00:00:00 2001 From: Razvan Codreanu <52859362+Schife@users.noreply.github.com> Date: Tue, 30 Jun 2020 20:12:27 +0100 Subject: [PATCH 40/85] INFRA-371 publish nightly and publish tag to docker hub steps (#6399) --- .ci/dev/integration/Jenkinsfile | 62 ---------------------- .ci/dev/nightly-regression/Jenkinsfile | 1 - .ci/dev/publish-branch/Jenkinsfile.nightly | 13 ++++- .ci/dev/publish-branch/Jenkinsfile.preview | 1 - .ci/dev/regression/Jenkinsfile | 16 +++++- .ci/dev/unit/Jenkinsfile | 60 --------------------- 6 files changed, 27 insertions(+), 126 deletions(-) delete mode 100644 .ci/dev/integration/Jenkinsfile delete mode 100644 .ci/dev/unit/Jenkinsfile diff --git a/.ci/dev/integration/Jenkinsfile b/.ci/dev/integration/Jenkinsfile deleted file mode 100644 index e8610ee716..0000000000 --- a/.ci/dev/integration/Jenkinsfile +++ /dev/null @@ -1,62 +0,0 @@ -import static com.r3.build.BuildControl.killAllExistingBuildsForJob -@Library('corda-shared-build-pipeline-steps') -import static com.r3.build.BuildControl.killAllExistingBuildsForJob - -killAllExistingBuildsForJob(env.JOB_NAME, env.BUILD_NUMBER.toInteger()) - -pipeline { - agent { label 'k8s' } - options { - timestamps() - timeout(time: 3, unit: 'HOURS') - } - - environment { - DOCKER_TAG_TO_USE = "${UUID.randomUUID().toString().toLowerCase().subSequence(0, 12)}" - EXECUTOR_NUMBER = "${env.EXECUTOR_NUMBER}" - BUILD_ID = "${env.BUILD_ID}-${env.JOB_NAME}" - } - - stages { - stage('Corda - Generate Build Image') { - steps { - withCredentials([string(credentialsId: 'container_reg_passwd', variable: 'DOCKER_PUSH_PWD')]) { - sh "./gradlew " + - "-Dkubenetize=true " + - "-Ddocker.push.password=\"\${DOCKER_PUSH_PWD}\" " + - "-Ddocker.work.dir=\"/tmp/\${EXECUTOR_NUMBER}\" " + - "-Ddocker.provided.tag=\"\${DOCKER_TAG_TO_USE}\"" + - " clean pushBuildImage" - } - sh "kubectl auth can-i get pods" - } - } - - stage('Corda - Run Tests') { - stage('Integration Tests') { - steps { - sh "./gradlew " + - "-DbuildId=\"\${BUILD_ID}\" " + - "-Dkubenetize=true " + - "-Ddocker.tag=\"\${DOCKER_TAG_TO_USE}\"" + - " allParallelIntegrationTest" - if (env.CHANGE_ID) { - pullRequest.createStatus(status: 'success', - context: 'continuous-integration/jenkins/pr-merge/integrationTest', - description: 'Integration Tests Passed', - targetUrl: "${env.JOB_URL}/testResults") - } - } - } - } - } - - post { - always { - junit testResults: '**/build/test-results-xml/**/*.xml', keepLongStdio: true - } - cleanup { - deleteDir() /* clean up our workspace */ - } - } -} \ No newline at end of file diff --git a/.ci/dev/nightly-regression/Jenkinsfile b/.ci/dev/nightly-regression/Jenkinsfile index 62b2fcd820..dc3979ae23 100644 --- a/.ci/dev/nightly-regression/Jenkinsfile +++ b/.ci/dev/nightly-regression/Jenkinsfile @@ -8,7 +8,6 @@ pipeline { options { timestamps() overrideIndexTriggers(false) - buildDiscarder(logRotator(daysToKeepStr: '7', artifactDaysToKeepStr: '7')) timeout(time: 3, unit: 'HOURS') } triggers { diff --git a/.ci/dev/publish-branch/Jenkinsfile.nightly b/.ci/dev/publish-branch/Jenkinsfile.nightly index 460117e500..f7a35981f6 100644 --- a/.ci/dev/publish-branch/Jenkinsfile.nightly +++ b/.ci/dev/publish-branch/Jenkinsfile.nightly @@ -11,7 +11,6 @@ pipeline { timestamps() ansiColor('xterm') overrideIndexTriggers(false) - buildDiscarder(logRotator(daysToKeepStr: '7', artifactDaysToKeepStr: '7')) timeout(time: 3, unit: 'HOURS') } @@ -24,6 +23,7 @@ pipeline { // in the name ARTIFACTORY_BUILD_NAME = "Corda / Publish / Publish Nightly to Artifactory" .replaceAll("/", " :: ") + DOCKER_URL = "https://index.docker.io/v1/" } stages { @@ -58,6 +58,17 @@ pipeline { ) } } + + stage('Publish Nightly to Docker Hub') { + steps { + withCredentials([ + usernamePassword(credentialsId: 'corda-publisher-docker-hub-credentials', + usernameVariable: 'DOCKER_USERNAME', + passwordVariable: 'DOCKER_PASSWORD')]) { + sh "./gradlew pushOfficialImages" + } + } + } } diff --git a/.ci/dev/publish-branch/Jenkinsfile.preview b/.ci/dev/publish-branch/Jenkinsfile.preview index 1b39ae3237..e0fb92aa77 100644 --- a/.ci/dev/publish-branch/Jenkinsfile.preview +++ b/.ci/dev/publish-branch/Jenkinsfile.preview @@ -11,7 +11,6 @@ pipeline { timestamps() ansiColor('xterm') overrideIndexTriggers(false) - buildDiscarder(logRotator(daysToKeepStr: '7', artifactDaysToKeepStr: '7')) timeout(time: 3, unit: 'HOURS') } diff --git a/.ci/dev/regression/Jenkinsfile b/.ci/dev/regression/Jenkinsfile index 271613a6ce..51f507dac2 100644 --- a/.ci/dev/regression/Jenkinsfile +++ b/.ci/dev/regression/Jenkinsfile @@ -37,12 +37,12 @@ pipeline { agent { label 'k8s' } options { timestamps() - buildDiscarder(logRotator(daysToKeepStr: '7', artifactDaysToKeepStr: '7')) timeout(time: 3, unit: 'HOURS') } environment { DOCKER_TAG_TO_USE = "${env.GIT_COMMIT.subSequence(0, 8)}" + DOCKER_URL = "https://index.docker.io/v1/" EXECUTOR_NUMBER = "${env.EXECUTOR_NUMBER}" BUILD_ID = "${env.BUILD_ID}-${env.JOB_NAME}" ARTIFACTORY_CREDENTIALS = credentials('artifactory-credentials') @@ -149,6 +149,20 @@ pipeline { ) } } + + stage('Publish Release to Docker Hub') { + when { + expression { isReleaseTag } + } + steps { + withCredentials([ + usernamePassword(credentialsId: 'corda-publisher-docker-hub-credentials', + usernameVariable: 'DOCKER_USERNAME', + passwordVariable: 'DOCKER_PASSWORD')]) { + sh "./gradlew pushOfficialImages" + } + } + } } diff --git a/.ci/dev/unit/Jenkinsfile b/.ci/dev/unit/Jenkinsfile deleted file mode 100644 index 14b93f7425..0000000000 --- a/.ci/dev/unit/Jenkinsfile +++ /dev/null @@ -1,60 +0,0 @@ -import static com.r3.build.BuildControl.killAllExistingBuildsForJob -@Library('corda-shared-build-pipeline-steps') -import static com.r3.build.BuildControl.killAllExistingBuildsForJob - -killAllExistingBuildsForJob(env.JOB_NAME, env.BUILD_NUMBER.toInteger()) - -pipeline { - agent { label 'k8s' } - options { - timestamps() - timeout(time: 3, unit: 'HOURS') - } - - environment { - DOCKER_TAG_TO_USE = "${UUID.randomUUID().toString().toLowerCase().subSequence(0, 12)}" - EXECUTOR_NUMBER = "${env.EXECUTOR_NUMBER}" - BUILD_ID = "${env.BUILD_ID}-${env.JOB_NAME}" - } - - stages { - stage('Corda Pull Request - Generate Build Image') { - steps { - withCredentials([string(credentialsId: 'container_reg_passwd', variable: 'DOCKER_PUSH_PWD')]) { - sh "./gradlew " + - "-Dkubenetize=true " + - "-Ddocker.push.password=\"\${DOCKER_PUSH_PWD}\" " + - "-Ddocker.work.dir=\"/tmp/\${EXECUTOR_NUMBER}\" " + - "-Ddocker.provided.tag=\"\${DOCKER_TAG_TO_USE}\"" + - " clean pushBuildImage" - } - sh "kubectl auth can-i get pods" - } - } - - stage('Unit Tests') { - steps { - sh "./gradlew " + - "-DbuildId=\"\${BUILD_ID}\" " + - "-Dkubenetize=true " + - "-Ddocker.tag=\"\${DOCKER_TAG_TO_USE}\"" + - " allParallelUnitTest" - if (env.CHANGE_ID) { - pullRequest.createStatus(status: 'success', - context: 'continuous-integration/jenkins/pr-merge/unitTest', - description: 'Unit Tests Passed', - targetUrl: "${env.JOB_URL}/testResults") - } - } - } - } - - post { - always { - junit testResults: '**/build/test-results-xml/**/*.xml', keepLongStdio: true - } - cleanup { - deleteDir() /* clean up our workspace */ - } - } -} \ No newline at end of file From 33cfa27f0b7a3cdc7b2f0a82ec2ba671ddefc8ad Mon Sep 17 00:00:00 2001 From: Razvan Codreanu <52859362+Schife@users.noreply.github.com> Date: Tue, 30 Jun 2020 20:13:58 +0100 Subject: [PATCH 41/85] INFRA-357 adding nexusIQ stage to JDK 11 regression job (#6421) --- .ci/dev/compatibility/JenkinsfileJDK11Azul | 34 +++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/.ci/dev/compatibility/JenkinsfileJDK11Azul b/.ci/dev/compatibility/JenkinsfileJDK11Azul index fd6767e1e0..4f04153ded 100644 --- a/.ci/dev/compatibility/JenkinsfileJDK11Azul +++ b/.ci/dev/compatibility/JenkinsfileJDK11Azul @@ -8,13 +8,26 @@ killAllExistingBuildsForJob(env.JOB_NAME, env.BUILD_NUMBER.toInteger()) */ boolean isReleaseTag = (env.TAG_NAME =~ /^release.*JDK11$/) +/* +** calculate the stage for NexusIQ evaluation +** * build for snapshots +** * stage-release: for release candidates and for health checks +** * operate: for final release +*/ +def nexusIqStage = "build" +if (isReleaseTag) { + switch (env.TAG_NAME) { + case ~/.*-RC\d+(-.*)?/: nexusIqStage = "stage-release"; break; + case ~/.*-HC\d+(-.*)?/: nexusIqStage = "stage-release"; break; + default: nexusIqStage = "operate" + } +} pipeline { agent { label 'k8s' } options { timestamps() - buildDiscarder(logRotator(daysToKeepStr: '7', artifactDaysToKeepStr: '7')) timeout(time: 3, unit: 'HOURS') } @@ -27,6 +40,25 @@ pipeline { } stages { + stage('Sonatype Check') { + steps { + sh "./gradlew --no-daemon clean jar" + script { + sh "./gradlew --no-daemon properties | grep -E '^(version|group):' >version-properties" + def version = sh (returnStdout: true, script: "grep ^version: version-properties | sed -e 's/^version: //'").trim() + def groupId = sh (returnStdout: true, script: "grep ^group: version-properties | sed -e 's/^group: //'").trim() + def artifactId = 'corda' + nexusAppId = "jenkins-${groupId}-${artifactId}-jdk11-${version}" + } + nexusPolicyEvaluation ( + failBuildOnNetworkError: false, + iqApplication: manualApplication(nexusAppId), + iqScanPatterns: [[scanPattern: 'node/capsule/build/libs/corda*.jar']], + iqStage: nexusIqStage + ) + } + } + stage('Generate Build Image') { steps { withCredentials([string(credentialsId: 'container_reg_passwd', variable: 'DOCKER_PUSH_PWD')]) { From 5499e2c050266d6a95dfb2075a8d2ebd47e0c188 Mon Sep 17 00:00:00 2001 From: Kyriakos Tharrouniatis Date: Wed, 1 Jul 2020 09:31:12 +0100 Subject: [PATCH 42/85] ENT-5384 Rename MAX_SQL_IN_CLAUSE_SET (#6414) Rename constant to `DEFAULT_SOFT_LOCKING_SQL_IN_CLAUSE_SIZE` --- .../net/corda/node/services/vault/NodeVaultService.kt | 4 ++-- .../corda/node/services/vault/NodeVaultServiceTest.kt | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt index 6dfd3b6e04..ad7d80059d 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt @@ -62,7 +62,7 @@ class NodeVaultService( companion object { private val log = contextLogger() - val MAX_SQL_IN_CLAUSE_SET = 16 + const val DEFAULT_SOFT_LOCKING_SQL_IN_CLAUSE_SIZE = 16 /** * Establish whether a given state is relevant to a node, given the node's public keys. @@ -870,7 +870,7 @@ private fun CriteriaBuilder.executeUpdate( var updatedRows = 0 it.asSequence() .map { stateRef -> PersistentStateRef(stateRef.txhash.bytes.toHexString(), stateRef.index) } - .chunked(NodeVaultService.MAX_SQL_IN_CLAUSE_SET) + .chunked(NodeVaultService.DEFAULT_SOFT_LOCKING_SQL_IN_CLAUSE_SIZE) .forEach { persistentStateRefs -> updatedRows += doUpdate(persistentStateRefs) } diff --git a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt index 320137e8b4..f2f141cac5 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt @@ -416,7 +416,7 @@ class NodeVaultServiceTest { } val softLockId = UUID.randomUUID() - val lockCount = NodeVaultService.MAX_SQL_IN_CLAUSE_SET * 2 + val lockCount = NodeVaultService.DEFAULT_SOFT_LOCKING_SQL_IN_CLAUSE_SIZE * 2 database.transaction { assertEquals(100, queryStates(SoftLockingType.UNLOCKED_ONLY).size) val unconsumedStates = vaultService.queryBy().states @@ -429,18 +429,18 @@ class NodeVaultServiceTest { assertEquals(lockCount, queryStates(SoftLockingType.LOCKED_ONLY).size) val unlockSet0 = mutableSetOf() - for (i in 0 until NodeVaultService.MAX_SQL_IN_CLAUSE_SET + 1) { + for (i in 0 until NodeVaultService.DEFAULT_SOFT_LOCKING_SQL_IN_CLAUSE_SIZE + 1) { unlockSet0.add(lockSet[i]) } vaultService.softLockRelease(softLockId, NonEmptySet.copyOf(unlockSet0)) - assertEquals(NodeVaultService.MAX_SQL_IN_CLAUSE_SET - 1, queryStates(SoftLockingType.LOCKED_ONLY).size) + assertEquals(NodeVaultService.DEFAULT_SOFT_LOCKING_SQL_IN_CLAUSE_SIZE - 1, queryStates(SoftLockingType.LOCKED_ONLY).size) val unlockSet1 = mutableSetOf() - for (i in NodeVaultService.MAX_SQL_IN_CLAUSE_SET + 1 until NodeVaultService.MAX_SQL_IN_CLAUSE_SET + 3) { + for (i in NodeVaultService.DEFAULT_SOFT_LOCKING_SQL_IN_CLAUSE_SIZE + 1 until NodeVaultService.DEFAULT_SOFT_LOCKING_SQL_IN_CLAUSE_SIZE + 3) { unlockSet1.add(lockSet[i]) } vaultService.softLockRelease(softLockId, NonEmptySet.copyOf(unlockSet1)) - assertEquals(NodeVaultService.MAX_SQL_IN_CLAUSE_SET - 1 - 2, queryStates(SoftLockingType.LOCKED_ONLY).size) + assertEquals(NodeVaultService.DEFAULT_SOFT_LOCKING_SQL_IN_CLAUSE_SIZE - 1 - 2, queryStates(SoftLockingType.LOCKED_ONLY).size) vaultService.softLockRelease(softLockId) // release the rest assertEquals(100, queryStates(SoftLockingType.UNLOCKED_ONLY).size) From 5f4401d16a1dcbeda688b30a1b118afed4a0d9a5 Mon Sep 17 00:00:00 2001 From: Dan Newton Date: Wed, 13 May 2020 15:17:51 +0100 Subject: [PATCH 43/85] NOTICK Non-database error handling in `withEntityManager` (#6239) When a non-database exception is thrown out of a `withEntityManager` block, always check if the session needs to be rolled back. This means if a database error is caught and a new non-database error is thrown out of the `withEntityManager` block, the transaction is still rolled back. The flow can then continue progressing as normal. --- .../corda/node/flows/FlowEntityManagerTest.kt | 126 ++++++++++++++++++ .../net/corda/node/internal/AbstractNode.kt | 7 +- 2 files changed, 129 insertions(+), 4 deletions(-) diff --git a/node/src/integration-test/kotlin/net/corda/node/flows/FlowEntityManagerTest.kt b/node/src/integration-test/kotlin/net/corda/node/flows/FlowEntityManagerTest.kt index f8c68f0cb6..add2317a64 100644 --- a/node/src/integration-test/kotlin/net/corda/node/flows/FlowEntityManagerTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/flows/FlowEntityManagerTest.kt @@ -32,6 +32,7 @@ import net.corda.testing.driver.driver import org.hibernate.exception.ConstraintViolationException import org.junit.Before import org.junit.Test +import java.lang.RuntimeException import java.sql.Connection import java.util.concurrent.ExecutorService import java.util.concurrent.Executors @@ -39,6 +40,7 @@ import java.util.concurrent.Semaphore import javax.persistence.PersistenceException import kotlin.test.assertEquals +@Suppress("TooGenericExceptionCaught", "TooGenericExceptionThrown") class FlowEntityManagerTest : AbstractFlowEntityManagerTest() { @Before @@ -364,6 +366,62 @@ class FlowEntityManagerTest : AbstractFlowEntityManagerTest() { } } + @Test(timeout = 300_000) + fun `non database error caught outside entity manager does not save entities`() { + var counter = 0 + StaffedFlowHospital.onFlowDischarged.add { _, _ -> ++counter } + + driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { + + val alice = startNode(providedName = ALICE_NAME).getOrThrow() + alice.rpc.startFlow(::EntityManagerSaveAndThrowNonDatabaseErrorFlow) + .returnValue.getOrThrow(30.seconds) + assertEquals(0, counter) + val entities = alice.rpc.startFlow(::GetCustomEntities).returnValue.getOrThrow() + assertEquals(0, entities.size) + + } + } + + @Test(timeout = 300_000) + fun `non database error caught outside entity manager after flush occurs does save entities`() { + var counter = 0 + StaffedFlowHospital.onFlowDischarged.add { _, _ -> ++counter } + + driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { + + val alice = startNode(providedName = ALICE_NAME).getOrThrow() + alice.rpc.startFlow(::EntityManagerSaveFlushAndThrowNonDatabaseErrorFlow) + .returnValue.getOrThrow(30.seconds) + assertEquals(0, counter) + val entities = alice.rpc.startFlow(::GetCustomEntities).returnValue.getOrThrow() + assertEquals(3, entities.size) + } + } + + @Test(timeout = 300_000) + fun `database error caught inside entity manager non database exception thrown and caught outside entity manager should not save entities`() { + var counter = 0 + StaffedFlowHospital.onFlowDischarged.add { _, _ -> ++counter } + + driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { + + val alice = startNode(providedName = ALICE_NAME).getOrThrow() + alice.rpc.expectFlowSuccessAndAssertCreatedEntities( + flow = ::EntityManagerCatchDatabaseErrorInsideEntityManagerThrowNonDatabaseErrorAndCatchOutsideFlow, + commitStatus = CommitStatus.NO_INTERMEDIATE_COMMIT, + numberOfDischarges = 0, + numberOfExpectedEntities = 1 + ) + alice.rpc.expectFlowSuccessAndAssertCreatedEntities( + flow = ::EntityManagerCatchDatabaseErrorInsideEntityManagerThrowNonDatabaseErrorAndCatchOutsideFlow, + commitStatus = CommitStatus.INTERMEDIATE_COMMIT, + numberOfDischarges = 0, + numberOfExpectedEntities = 1 + ) + } + } + @StartableByRPC class EntityManagerSaveEntitiesWithoutAFlushFlow : FlowLogic() { @@ -706,6 +764,74 @@ class FlowEntityManagerTest : AbstractFlowEntityManagerTest() { } } + @StartableByRPC + class EntityManagerSaveAndThrowNonDatabaseErrorFlow : FlowLogic() { + + @Suspendable + override fun call() { + try { + serviceHub.withEntityManager { + persist(entityWithIdOne) + persist(entityWithIdTwo) + persist(entityWithIdThree) + throw RuntimeException("die") + } + } catch (e: RuntimeException) { + logger.info("Caught error") + } + sleep(1.millis) + } + } + + @StartableByRPC + class EntityManagerSaveFlushAndThrowNonDatabaseErrorFlow : FlowLogic() { + + @Suspendable + override fun call() { + try { + serviceHub.withEntityManager { + persist(entityWithIdOne) + persist(entityWithIdTwo) + persist(entityWithIdThree) + flush() + throw RuntimeException("die") + } + } catch (e: RuntimeException) { + logger.info("Caught error") + } + sleep(1.millis) + } + } + + @StartableByRPC + class EntityManagerCatchDatabaseErrorInsideEntityManagerThrowNonDatabaseErrorAndCatchOutsideFlow(private val commitStatus: CommitStatus) : + FlowLogic() { + + @Suspendable + override fun call() { + serviceHub.withEntityManager { + persist(entityWithIdOne) + } + if (commitStatus == CommitStatus.INTERMEDIATE_COMMIT) { + sleep(1.millis) + } + try { + serviceHub.withEntityManager { + persist(anotherEntityWithIdOne) + try { + flush() + } catch (e: PersistenceException) { + logger.info("Caught the exception!") + } + throw RuntimeException("die") + } + } catch (e: RuntimeException) { + logger.info("Caught error") + } + sleep(1.millis) + } + } + @CordaService class MyService(private val services: AppServiceHub) : SingletonSerializeAsToken() { diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 462d310c78..83e35807a6 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -180,7 +180,7 @@ import java.sql.Savepoint import java.time.Clock import java.time.Duration import java.time.format.DateTimeParseException -import java.util.* +import java.util.Properties import java.util.concurrent.ExecutorService import java.util.concurrent.Executors import java.util.concurrent.LinkedBlockingQueue @@ -190,8 +190,6 @@ import java.util.concurrent.TimeUnit.MINUTES import java.util.concurrent.TimeUnit.SECONDS import java.util.function.Consumer import javax.persistence.EntityManager -import javax.persistence.PersistenceException -import kotlin.collections.ArrayList /** * A base node implementation that can be customised either for production (with real implementations that do real @@ -1166,6 +1164,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, override fun jdbcSession(): Connection = database.createSession() + @Suppress("TooGenericExceptionCaught") override fun withEntityManager(block: EntityManager.() -> T): T { return database.transaction(useErrorHandler = false) { session.flush() @@ -1180,7 +1179,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, connection.rollback(savepoint) } } - } catch (e: PersistenceException) { + } catch (e: Exception) { if (manager.transaction.rollbackOnly) { connection.rollback(savepoint) } From 9f12e6bbc59fdcc91bc3c735f0788a3e927a7eb0 Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Wed, 1 Jul 2020 21:47:09 +0000 Subject: [PATCH 44/85] INFRA-433 Rebuild node config parsing tests (#6423) Replace node configuration parsing tests with lighter weight equivalents which just parse the configuration rather than starting a full node. --- .../net/corda/node/NodeConfigParsingTests.kt | 110 ------------------ .../services/config/NodeConfigParsingTests.kt | 32 +++++ .../node/services/config/ConfigUtilities.kt | 36 +++++- .../corda/node/internal/NodeStartupTest.kt | 5 +- .../node/services/config/ConfigHelperTests.kt | 83 +++++++++++++ 5 files changed, 147 insertions(+), 119 deletions(-) delete mode 100644 node/src/integration-test/kotlin/net/corda/node/NodeConfigParsingTests.kt create mode 100644 node/src/integration-test/kotlin/net/corda/node/services/config/NodeConfigParsingTests.kt create mode 100644 node/src/test/kotlin/net/corda/node/services/config/ConfigHelperTests.kt diff --git a/node/src/integration-test/kotlin/net/corda/node/NodeConfigParsingTests.kt b/node/src/integration-test/kotlin/net/corda/node/NodeConfigParsingTests.kt deleted file mode 100644 index fd2f7d7507..0000000000 --- a/node/src/integration-test/kotlin/net/corda/node/NodeConfigParsingTests.kt +++ /dev/null @@ -1,110 +0,0 @@ -package net.corda.node - -import net.corda.core.utilities.getOrThrow -import net.corda.node.logging.logFile -import net.corda.testing.driver.DriverParameters -import net.corda.testing.driver.NodeParameters -import net.corda.testing.driver.driver -import net.corda.testing.driver.internal.incrementalPortAllocation -import org.assertj.core.api.Assertions.assertThatThrownBy -import org.junit.Test -import org.junit.Assert.assertTrue - - -class NodeConfigParsingTests { - - @Test(timeout=300_000) - fun `config is overriden by underscore variable`() { - val portAllocator = incrementalPortAllocation() - val sshPort = portAllocator.nextPort() - - driver(DriverParameters( - environmentVariables = mapOf("corda_sshd_port" to sshPort.toString()), - startNodesInProcess = false, - portAllocation = portAllocator)) { - val hasSsh = startNode().get() - .logFile() - .readLines() - .filter { it.contains("SSH server listening on port") } - .any { it.contains(sshPort.toString()) } - assertTrue(hasSsh) - } - } - - @Test(timeout=300_000) - fun `config is overriden by case insensitive underscore variable`() { - val portAllocator = incrementalPortAllocation() - val sshPort = portAllocator.nextPort() - - driver(DriverParameters( - environmentVariables = mapOf("CORDA_sshd_port" to sshPort.toString()), - startNodesInProcess = false, - portAllocation = portAllocator)) { - val hasSsh = startNode().get() - .logFile() - .readLines() - .filter { it.contains("SSH server listening on port") } - .any { it.contains(sshPort.toString()) } - assertTrue(hasSsh) - } - } - - @Test(timeout=300_000) - fun `config is overriden by case insensitive dot variable`() { - val portAllocator = incrementalPortAllocation() - val sshPort = portAllocator.nextPort() - - driver(DriverParameters( - environmentVariables = mapOf("CORDA.sshd.port" to sshPort.toString(), - "corda.devMode" to true.toString()), - startNodesInProcess = false, - portAllocation = portAllocator)) { - val hasSsh = startNode(NodeParameters()).get() - .logFile() - .readLines() - .filter { it.contains("SSH server listening on port") } - .any { it.contains(sshPort.toString()) } - assertTrue(hasSsh) - } - } - - @Test(timeout=300_000) - fun `shadowing is forbidden`() { - val portAllocator = incrementalPortAllocation() - val sshPort = portAllocator.nextPort() - - driver(DriverParameters( - environmentVariables = mapOf( - "CORDA_sshd_port" to sshPort.toString(), - "corda.sshd.port" to sshPort.toString()), - startNodesInProcess = false, - portAllocation = portAllocator, - notarySpecs = emptyList())) { - - assertThatThrownBy { - startNode().getOrThrow() - } - } - } - - @Test(timeout=300_000) - fun `bad keys are ignored and warned for`() { - val portAllocator = incrementalPortAllocation() - driver(DriverParameters( - environmentVariables = mapOf( - "corda_bad_key" to "2077"), - startNodesInProcess = false, - portAllocation = portAllocator, - notarySpecs = emptyList())) { - - val hasWarning = startNode() - .getOrThrow() - .logFile() - .readLines() - .any { - it.contains("(property or environment variable) cannot be mapped to an existing Corda") - } - assertTrue(hasWarning) - } - } -} diff --git a/node/src/integration-test/kotlin/net/corda/node/services/config/NodeConfigParsingTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/config/NodeConfigParsingTests.kt new file mode 100644 index 0000000000..2f704bf630 --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/node/services/config/NodeConfigParsingTests.kt @@ -0,0 +1,32 @@ +package net.corda.node.services.config + +import net.corda.core.utilities.getOrThrow +import net.corda.node.logging.logFile +import net.corda.testing.driver.DriverParameters +import net.corda.testing.driver.driver +import net.corda.testing.driver.internal.incrementalPortAllocation +import org.junit.Assert.assertTrue +import org.junit.Test + +class NodeConfigParsingTests { + @Test(timeout = 300_000) + fun `bad keys are ignored and warned for`() { + val portAllocator = incrementalPortAllocation() + driver(DriverParameters( + environmentVariables = mapOf( + "corda_bad_key" to "2077"), + startNodesInProcess = false, + portAllocation = portAllocator, + notarySpecs = emptyList())) { + + val hasWarning = startNode() + .getOrThrow() + .logFile() + .readLines() + .any { + it.contains("(property or environment variable) cannot be mapped to an existing Corda") + } + assertTrue(hasWarning) + } + } +} diff --git a/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt b/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt index 6aaac65251..d4e0239537 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt @@ -6,6 +6,7 @@ import com.typesafe.config.ConfigParseOptions import net.corda.cliutils.CordaSystemUtils import net.corda.common.configuration.parsing.internal.Configuration import net.corda.core.identity.CordaX500Name +import net.corda.core.internal.VisibleForTesting import net.corda.core.internal.createDirectories import net.corda.core.internal.div import net.corda.core.internal.exists @@ -36,11 +37,34 @@ object ConfigHelper { private const val UPPERCASE_PROPERTY_PREFIX = "CORDA." private val log = LoggerFactory.getLogger(javaClass) + + val DEFAULT_CONFIG_FILENAME = "node.conf" + @Suppress("LongParameterList") fun loadConfig(baseDirectory: Path, - configFile: Path = baseDirectory / "node.conf", + configFile: Path = baseDirectory / DEFAULT_CONFIG_FILENAME, allowMissingConfig: Boolean = false, - configOverrides: Config = ConfigFactory.empty()): Config { + configOverrides: Config = ConfigFactory.empty()): Config + = loadConfig(baseDirectory, + configFile = configFile, + allowMissingConfig = allowMissingConfig, + configOverrides = configOverrides, + rawSystemOverrides = ConfigFactory.systemProperties(), + rawEnvironmentOverrides = ConfigFactory.systemEnvironment()) + + /** + * Internal equivalent of [loadConfig] which allows the system and environment + * overrides to be provided from a test. + */ + @Suppress("LongParameterList") + @VisibleForTesting + internal fun loadConfig(baseDirectory: Path, + configFile: Path, + allowMissingConfig: Boolean, + configOverrides: Config, + rawSystemOverrides: Config, + rawEnvironmentOverrides: Config + ): Config { val parseOptions = ConfigParseOptions.defaults() val defaultConfig = ConfigFactory.parseResources("corda-reference.conf", parseOptions.setAllowMissing(false)) val appConfig = ConfigFactory.parseFile(configFile.toFile(), parseOptions.setAllowMissing(allowMissingConfig)) @@ -55,8 +79,8 @@ object ConfigHelper { "flowExternalOperationThreadPoolSize" to min(coreCount, FLOW_EXTERNAL_OPERATION_THREAD_POOL_SIZE_MAX).toString() ) - val systemOverrides = ConfigFactory.systemProperties().cordaEntriesOnly() - val environmentOverrides = ConfigFactory.systemEnvironment().cordaEntriesOnly() + val systemOverrides = rawSystemOverrides.cordaEntriesOnly() + val environmentOverrides = rawEnvironmentOverrides.cordaEntriesOnly() val finalConfig = configOf( // Add substitution values here "baseDirectory" to baseDirectory.toString()) @@ -91,8 +115,8 @@ object ConfigHelper { .mapKeys { val original = it.key as String - // Reject environment variable that are in all caps - // since these cannot be properties. + // Reject environment variable that are in all caps + // since these cannot be properties. if (original == original.toUpperCase()){ return@mapKeys original } diff --git a/node/src/test/kotlin/net/corda/node/internal/NodeStartupTest.kt b/node/src/test/kotlin/net/corda/node/internal/NodeStartupTest.kt index ccaa925268..da793ee97c 100644 --- a/node/src/test/kotlin/net/corda/node/internal/NodeStartupTest.kt +++ b/node/src/test/kotlin/net/corda/node/internal/NodeStartupTest.kt @@ -1,9 +1,9 @@ package net.corda.node.internal -import com.google.common.io.Files import org.assertj.core.api.Assertions.assertThat import org.junit.Test import java.nio.channels.OverlappingFileLockException +import java.nio.file.Files import java.util.concurrent.CountDownLatch import kotlin.concurrent.thread import kotlin.test.assertFailsWith @@ -11,8 +11,7 @@ import kotlin.test.assertFailsWith class NodeStartupTest { @Test(timeout=300_000) fun `test that you cant start two nodes in the same directory`() { - val dir = Files.createTempDir().toPath() - + val dir = Files.createTempDirectory("node_startup_test") val latch = CountDownLatch(1) thread(start = true) { diff --git a/node/src/test/kotlin/net/corda/node/services/config/ConfigHelperTests.kt b/node/src/test/kotlin/net/corda/node/services/config/ConfigHelperTests.kt new file mode 100644 index 0000000000..cd378de825 --- /dev/null +++ b/node/src/test/kotlin/net/corda/node/services/config/ConfigHelperTests.kt @@ -0,0 +1,83 @@ +package net.corda.node.services.config + +import com.typesafe.config.Config +import com.typesafe.config.ConfigFactory +import net.corda.core.internal.delete +import net.corda.core.internal.div +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import java.nio.file.Files +import java.nio.file.Path + +class ConfigHelperTests { + private var baseDir: Path? = null + + @Before + fun setup() { + baseDir = Files.createTempDirectory("corda_config") + } + + @After + fun cleanup() { + baseDir?.delete() + } + + @Test(timeout = 300_000) + fun `config is overridden by underscore variable`() { + val sshPort: Long = 9000 + + // Verify the port isn't set when not provided + var config = loadConfig() + Assert.assertFalse("SSH port should not be configured when not provided", config!!.hasPath("sshd.port")) + + config = loadConfig("corda_sshd_port" to sshPort) + Assert.assertEquals(sshPort, config?.getLong("sshd.port")) + } + + @Test(timeout = 300_000) + fun `config is overridden by case insensitive underscore variable`() { + val sshPort: Long = 10000 + val config = loadConfig("CORDA_sshd_port" to sshPort) + Assert.assertEquals(sshPort, config?.getLong("sshd.port")) + } + + @Test(timeout = 300_000) + fun `config is overridden by case insensitive dot variable`() { + val sshPort: Long = 11000 + val config = loadConfig("CORDA.sshd.port" to sshPort, + "corda.devMode" to true.toString()) + Assert.assertEquals(sshPort, config?.getLong("sshd.port")) + } + + @Test(timeout = 300_000, expected = ShadowingException::class) + fun `shadowing is forbidden`() { + val sshPort: Long = 12000 + loadConfig("CORDA_sshd_port" to sshPort.toString(), + "corda.sshd.port" to sshPort.toString()) + } + + /** + * Load the node configuration with the given environment variable + * overrides. + * + * @param environmentVariables pairs of keys and values for environment variables + * to simulate when loading the configuration. + */ + @Suppress("SpreadOperator") + private fun loadConfig(vararg environmentVariables: Pair): Config? { + return baseDir?.let { + ConfigHelper.loadConfig( + baseDirectory = it, + configFile = it / ConfigHelper.DEFAULT_CONFIG_FILENAME, + allowMissingConfig = true, + configOverrides = ConfigFactory.empty(), + rawSystemOverrides = ConfigFactory.empty(), + rawEnvironmentOverrides = ConfigFactory.empty().plus( + mapOf(*environmentVariables) + ) + ) + } + } +} From adc0879e8ead83284cf45d57b4c2738f909f3364 Mon Sep 17 00:00:00 2001 From: Denis Rekalov Date: Thu, 2 Jul 2020 09:20:23 +0100 Subject: [PATCH 45/85] CORDA-3867: Add tests for AMQ_VALIDATED_USER (#6418) * CORDA-3867: Add tests for AMQ_VALIDATED_USER * CORDA-3867: detekt --- detekt-baseline.xml | 1 + node/build.gradle | 3 +- .../messaging/MQSecurityAsNodeTest.kt | 44 +++++- .../services/messaging/MQSecurityTest.kt | 25 ++-- .../services/messaging/P2PMQSecurityTest.kt | 28 +++- .../services/messaging/SimpleAMQPClient.kt | 141 ++++++++++++++++++ 6 files changed, 224 insertions(+), 18 deletions(-) create mode 100644 node/src/integration-test/kotlin/net/corda/services/messaging/SimpleAMQPClient.kt diff --git a/detekt-baseline.xml b/detekt-baseline.xml index 9471d7b8c1..8e72535cd4 100644 --- a/detekt-baseline.xml +++ b/detekt-baseline.xml @@ -1598,6 +1598,7 @@ TooGenericExceptionCaught:ScheduledFlowIntegrationTests.kt$ScheduledFlowIntegrationTests$ex: Exception TooGenericExceptionCaught:SerializationOutputTests.kt$SerializationOutputTests$t: Throwable TooGenericExceptionCaught:ShutdownManager.kt$ShutdownManager$t: Throwable + TooGenericExceptionCaught:SimpleAMQPClient.kt$SimpleAMQPClient$e: Exception TooGenericExceptionCaught:SimpleMQClient.kt$SimpleMQClient$e: Exception TooGenericExceptionCaught:SingleThreadedStateMachineManager.kt$SingleThreadedStateMachineManager$e: Exception TooGenericExceptionCaught:SingleThreadedStateMachineManager.kt$SingleThreadedStateMachineManager$ex: Exception diff --git a/node/build.gradle b/node/build.gradle index 58f7a5498e..60e071bbb6 100644 --- a/node/build.gradle +++ b/node/build.gradle @@ -191,7 +191,8 @@ dependencies { // Integration test helpers integrationTestCompile "junit:junit:$junit_version" integrationTestCompile "org.assertj:assertj-core:${assertj_version}" - + integrationTestCompile "org.apache.qpid:qpid-jms-client:${protonj_version}" + // BFT-Smart dependencies compile 'com.github.bft-smart:library:master-v1.1-beta-g6215ec8-87' compile 'commons-codec:commons-codec:1.13' diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsNodeTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsNodeTest.kt index 923bb0d794..1e52c27ed6 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsNodeTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsNodeTest.kt @@ -16,6 +16,7 @@ import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.loadDevCaTrustStore import net.corda.coretesting.internal.stubs.CertificateStoreStubs import net.corda.nodeapi.internal.ArtemisMessagingComponent +import net.corda.services.messaging.SimpleAMQPClient.Companion.sendAndVerify import net.corda.testing.core.singleIdentity import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration import org.apache.activemq.artemis.api.core.ActiveMQClusterSecurityException @@ -25,9 +26,9 @@ import org.assertj.core.api.Assertions.assertThatExceptionOfType import org.bouncycastle.asn1.x509.GeneralName import org.bouncycastle.asn1.x509.GeneralSubtree import org.bouncycastle.asn1.x509.NameConstraints -import org.junit.Ignore import org.junit.Test import java.nio.file.Files +import javax.jms.JMSSecurityException import kotlin.test.assertEquals /** @@ -42,10 +43,9 @@ class MQSecurityAsNodeTest : P2PMQSecurityTest() { attacker.start(PEER_USER, PEER_USER) // Login as a peer } - @Ignore("Core protocol messages are no allowed for PEER_USER: need to switch to AMQP") @Test(timeout=300_000) fun `send message to RPC requests address`() { - assertSendAttackFails(RPCApi.RPC_SERVER_QUEUE_NAME) + assertProducerQueueCreationAttackFails(RPCApi.RPC_SERVER_QUEUE_NAME) } @Test(timeout=300_000) @@ -124,16 +124,52 @@ class MQSecurityAsNodeTest : P2PMQSecurityTest() { } } - @Ignore("Core protocol messages are no allowed for PEER_USER: need to switch to AMQP") override fun `send message to notifications address`() { + assertProducerQueueCreationAttackFails(ArtemisMessagingComponent.NOTIFICATIONS_ADDRESS) } @Test(timeout=300_000) fun `send message on core protocol`() { + val attacker = clientTo(alice.node.configuration.p2pAddress) + attacker.start(PEER_USER, PEER_USER) val message = attacker.createMessage() assertEquals(true, attacker.producer.isBlockOnNonDurableSend) assertThatExceptionOfType(ActiveMQSecurityException::class.java).isThrownBy { attacker.producer.send("${ArtemisMessagingComponent.P2P_PREFIX}${alice.info.singleIdentity().owningKey.toStringShort()}", message) }.withMessageContaining("CoreMessage").withMessageContaining("AMQPMessage") } + + @Test(timeout = 300_000) + fun `send AMQP message with correct validated user in header`() { + val attacker = amqpClientTo(alice.node.configuration.p2pAddress) + val session = attacker.start(PEER_USER, PEER_USER) + val message = session.createMessage() + message.setStringProperty("_AMQ_VALIDATED_USER", "O=MegaCorp, L=London, C=GB") + val queue = session.createQueue("${ArtemisMessagingComponent.P2P_PREFIX}${alice.info.singleIdentity().owningKey.toStringShort()}") + val producer = session.createProducer(queue) + producer.sendAndVerify(message) + } + + @Test(timeout = 300_000) + fun `send AMQP message with incorrect validated user in header`() { + val attacker = amqpClientTo(alice.node.configuration.p2pAddress) + val session = attacker.start(PEER_USER, PEER_USER) + val message = session.createMessage() + message.setStringProperty("_AMQ_VALIDATED_USER", "O=Bob, L=New York, C=US") + val queue = session.createQueue("${ArtemisMessagingComponent.P2P_PREFIX}${alice.info.singleIdentity().owningKey.toStringShort()}") + val producer = session.createProducer(queue) + assertThatExceptionOfType(JMSSecurityException::class.java).isThrownBy { + producer.sendAndVerify(message) + }.withMessageContaining("_AMQ_VALIDATED_USER mismatch") + } + + @Test(timeout = 300_000) + fun `send AMQP message without header`() { + val attacker = amqpClientTo(alice.node.configuration.p2pAddress) + val session = attacker.start(PEER_USER, PEER_USER) + val message = session.createMessage() + val queue = session.createQueue("${ArtemisMessagingComponent.P2P_PREFIX}${alice.info.singleIdentity().owningKey.toStringShort()}") + val producer = session.createProducer(queue) + producer.sendAndVerify(message) + } } diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt index 3fbaa29a37..82c9804b8f 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt @@ -45,7 +45,7 @@ abstract class MQSecurityTest : NodeBasedTest() { private val rpcUser = User("user1", "pass", permissions = emptySet()) lateinit var alice: NodeWithInfo lateinit var attacker: SimpleMQClient - private val clients = ArrayList() + private val runOnStop = ArrayList<() -> Any?>() @Before override fun setUp() { @@ -62,8 +62,8 @@ abstract class MQSecurityTest : NodeBasedTest() { abstract fun startAttacker(attacker: SimpleMQClient) @After - fun stopClients() { - clients.forEach { it.stop() } + fun tearDown() { + runOnStop.forEach { it() } } @Test(timeout=300_000) @@ -97,18 +97,21 @@ abstract class MQSecurityTest : NodeBasedTest() { fun clientTo(target: NetworkHostAndPort, sslConfiguration: MutualSslConfiguration? = configureTestSSL(CordaX500Name("MegaCorp", "London", "GB"))): SimpleMQClient { val client = SimpleMQClient(target, sslConfiguration) - clients += client + runOnStop += client::stop + return client + } + + fun amqpClientTo(target: NetworkHostAndPort, + sslConfiguration: MutualSslConfiguration = configureTestSSL(CordaX500Name("MegaCorp", "London", "GB")) + ): SimpleAMQPClient { + val client = SimpleAMQPClient(target, sslConfiguration) + runOnStop += client::stop return client } private val rpcConnections = mutableListOf() private fun loginToRPC(target: NetworkHostAndPort, rpcUser: User): CordaRPCOps { - return CordaRPCClient(target).start(rpcUser.username, rpcUser.password).also { rpcConnections.add(it) }.proxy - } - - @After - fun closeRPCConnections() { - rpcConnections.forEach { it.forceClose() } + return CordaRPCClient(target).start(rpcUser.username, rpcUser.password).also { runOnStop += it::forceClose }.proxy } fun loginToRPCAndGetClientQueue(): String { @@ -152,7 +155,7 @@ abstract class MQSecurityTest : NodeBasedTest() { } } - fun assertSendAttackFails(address: String) { + open fun assertSendAttackFails(address: String) { val message = attacker.createMessage() assertEquals(true, attacker.producer.isBlockOnNonDurableSend) assertAttackFails(address, "SEND") { diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMQSecurityTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMQSecurityTest.kt index 068c521a6d..1773b03380 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMQSecurityTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMQSecurityTest.kt @@ -3,18 +3,43 @@ package net.corda.services.messaging import net.corda.core.crypto.generateKeyPair import net.corda.core.crypto.toStringShort import net.corda.nodeapi.RPCApi +import net.corda.nodeapi.internal.ArtemisMessagingComponent import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_PREFIX import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEERS_PREFIX +import net.corda.services.messaging.SimpleAMQPClient.Companion.sendAndVerify import net.corda.testing.core.BOB_NAME import net.corda.testing.core.singleIdentity -import org.junit.Ignore +import org.assertj.core.api.Assertions.assertThatExceptionOfType import org.junit.Test +import javax.jms.JMSException /** * Runs a series of MQ-related attacks against a node. Subclasses need to call [startAttacker] to connect * the attacker to [alice]. */ abstract class P2PMQSecurityTest : MQSecurityTest() { + override fun assertSendAttackFails(address: String) { + val attacker = amqpClientTo(alice.node.configuration.p2pAddress) + val session = attacker.start(ArtemisMessagingComponent.PEER_USER, ArtemisMessagingComponent.PEER_USER) + val message = session.createMessage() + message.setStringProperty("_AMQ_VALIDATED_USER", "O=MegaCorp, L=London, C=GB") + val queue = session.createQueue(address) + assertThatExceptionOfType(JMSException::class.java).isThrownBy { + session.createProducer(queue).sendAndVerify(message) + }.withMessageContaining(address).withMessageContaining("SEND") + } + + fun assertProducerQueueCreationAttackFails(address: String) { + val attacker = amqpClientTo(alice.node.configuration.p2pAddress) + val session = attacker.start(ArtemisMessagingComponent.PEER_USER, ArtemisMessagingComponent.PEER_USER) + val message = session.createMessage() + message.setStringProperty("_AMQ_VALIDATED_USER", "O=MegaCorp, L=London, C=GB") + val queue = session.createQueue(address) + assertThatExceptionOfType(JMSException::class.java).isThrownBy { + session.createProducer(queue) + }.withMessageContaining(address).withMessageContaining("CREATE_DURABLE_QUEUE") + } + @Test(timeout=300_000) fun `consume message from P2P queue`() { assertConsumeAttackFails("$P2P_PREFIX${alice.info.singleIdentity().owningKey.toStringShort()}") @@ -26,7 +51,6 @@ abstract class P2PMQSecurityTest : MQSecurityTest() { assertConsumeAttackFails("$PEERS_PREFIX${bobParty.owningKey.toStringShort()}") } - @Ignore("Core protocol messages are no allowed for PEER_USER: need to switch to AMQP") @Test(timeout=300_000) fun `send message to address of peer which has been communicated with`() { val bobParty = startBobAndCommunicateWithAlice() diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/SimpleAMQPClient.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/SimpleAMQPClient.kt new file mode 100644 index 0000000000..bb3c86e9de --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/SimpleAMQPClient.kt @@ -0,0 +1,141 @@ +package net.corda.services.messaging + +import net.corda.core.internal.concurrent.openFuture +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.nodeapi.internal.config.MutualSslConfiguration +import org.apache.qpid.jms.JmsConnectionFactory +import org.apache.qpid.jms.meta.JmsConnectionInfo +import org.apache.qpid.jms.provider.Provider +import org.apache.qpid.jms.provider.ProviderFuture +import org.apache.qpid.jms.provider.amqp.AmqpProvider +import org.apache.qpid.jms.provider.amqp.AmqpSaslAuthenticator +import org.apache.qpid.jms.sasl.PlainMechanism +import org.apache.qpid.jms.transports.TransportOptions +import org.apache.qpid.jms.transports.netty.NettyTcpTransport +import org.apache.qpid.proton.engine.Sasl +import org.apache.qpid.proton.engine.SaslListener +import org.apache.qpid.proton.engine.Transport +import java.net.URI +import java.security.SecureRandom +import java.util.concurrent.ExecutionException +import java.util.concurrent.TimeUnit +import javax.jms.CompletionListener +import javax.jms.Connection +import javax.jms.Message +import javax.jms.MessageProducer +import javax.jms.Session +import javax.net.ssl.KeyManagerFactory +import javax.net.ssl.SSLContext +import javax.net.ssl.TrustManagerFactory + +/** + * Simple AMQP client connecting to broker using JMS. + */ +class SimpleAMQPClient(private val target: NetworkHostAndPort, private val config: MutualSslConfiguration) { + companion object { + /** + * Send message and wait for completion. + * @throws Exception on failure + */ + fun MessageProducer.sendAndVerify(message: Message) { + val request = openFuture() + send(message, object : CompletionListener { + override fun onException(message: Message, exception: Exception) { + request.setException(exception) + } + + override fun onCompletion(message: Message) { + request.set(Unit) + } + }) + try { + request.get(10, TimeUnit.SECONDS) + } catch (e: ExecutionException) { + throw e.cause!! + } + } + } + + private lateinit var connection: Connection + + private fun sslContext(): SSLContext { + val keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()).apply { + init(config.keyStore.get().value.internal, config.keyStore.entryPassword.toCharArray()) + } + val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()).apply { + init(config.trustStore.get().value.internal) + } + val sslContext = SSLContext.getInstance("TLS") + val keyManagers = keyManagerFactory.keyManagers + val trustManagers = trustManagerFactory.trustManagers + sslContext.init(keyManagers, trustManagers, SecureRandom()) + return sslContext + } + + fun start(username: String, password: String): Session { + val connectionFactory = TestJmsConnectionFactory("amqps://${target.host}:${target.port}", username, password) + connectionFactory.setSslContext(sslContext()) + connection = connectionFactory.createConnection() + connection.start() + return connection.createSession(false, Session.AUTO_ACKNOWLEDGE) + } + + fun stop() { + try { + connection.close() + } catch (e: Exception) { + // connection might not have initialised. + } + } + + private class TestJmsConnectionFactory(uri: String, private val user: String, private val pwd: String) : JmsConnectionFactory(uri) { + override fun createProvider(remoteURI: URI): Provider { + val transportOptions = TransportOptions().apply { + // Disable SNI check for server certificate + isVerifyHost = false + } + val transport = NettyTcpTransport(remoteURI, transportOptions, true) + + // Manually override SASL negotiations to accept failure in SASL-OUTCOME, which is produced by node Artemis server + return object : AmqpProvider(remoteURI, transport) { + override fun connect(connectionInfo: JmsConnectionInfo?) { + super.connect(connectionInfo) + val sasl = protonTransport.sasl() + sasl.client() + sasl.setRemoteHostname(remoteURI.host) + val authenticator = AmqpSaslAuthenticator { + PlainMechanism().apply { + username = user + password = pwd + } + } + val saslRequest = ProviderFuture() + sasl.setListener(object : SaslListener { + override fun onSaslMechanisms(sasl: Sasl, transport: Transport) { + authenticator.handleSaslMechanisms(sasl, transport) + } + + override fun onSaslChallenge(sasl: Sasl, transport: Transport) { + authenticator.handleSaslChallenge(sasl, transport) + } + + override fun onSaslOutcome(sasl: Sasl, transport: Transport) { + authenticator.handleSaslOutcome(sasl, transport) + saslRequest.onSuccess() + } + + override fun onSaslInit(sasl: Sasl, transport: Transport) { + } + + override fun onSaslResponse(sasl: Sasl, transport: Transport) { + } + }) + pumpToProtonTransport() + saslRequest.sync() + } + }.apply { + isSaslLayer = false + } + } + } +} From d97bc7dd1245ba7a06435e8b629f2c1b605fa3de Mon Sep 17 00:00:00 2001 From: Tamas Veingartner Date: Thu, 2 Jul 2020 11:35:04 +0100 Subject: [PATCH 46/85] ENT-5420 stop reconnecting when RejectedCommandException occurs. (#6394) * ENT-5420 stop reconnecting when RejectedCommandException occurs. * change to store last exception and return quietly * changed to rethrow exception. Test added --- .../CordaRPCClientReconnectionTest.kt | 33 +++++++++++++++++++ .../rpc/internal/ReconnectingCordaRPCOps.kt | 4 +-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/client/rpc/src/integration-test/kotlin/net/corda/client/rpcreconnect/CordaRPCClientReconnectionTest.kt b/client/rpc/src/integration-test/kotlin/net/corda/client/rpcreconnect/CordaRPCClientReconnectionTest.kt index 70ae13731f..fbf55e194b 100644 --- a/client/rpc/src/integration-test/kotlin/net/corda/client/rpcreconnect/CordaRPCClientReconnectionTest.kt +++ b/client/rpc/src/integration-test/kotlin/net/corda/client/rpcreconnect/CordaRPCClientReconnectionTest.kt @@ -15,6 +15,7 @@ import net.corda.finance.DOLLARS import net.corda.finance.contracts.asset.Cash import net.corda.finance.flows.CashIssueFlow import net.corda.node.services.Permissions +import net.corda.nodeapi.exceptions.RejectedCommandException import net.corda.testing.core.CHARLIE_NAME import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.NodeHandle @@ -49,6 +50,38 @@ class CordaRPCClientReconnectionTest { val rpcUser = User("user1", "test", permissions = setOf(Permissions.all())) } + + + + + @Test(timeout=300_000) + fun `rpc node start when FlowsDrainingModeEnabled throws RejectedCommandException and won't attempt to reconnect`() { + driver(DriverParameters(cordappsForAllNodes = FINANCE_CORDAPPS)) { + val address = NetworkHostAndPort("localhost", portAllocator.nextPort()) + + fun startNode(): NodeHandle { + return startNode( + providedName = CHARLIE_NAME, + rpcUsers = listOf(CordaRPCClientTest.rpcUser), + customOverrides = mapOf("rpcSettings.address" to address.toString()) + ).getOrThrow() + } + + val node = startNode() + val client = CordaRPCClient(node.rpcAddress, + config.copy(maxReconnectAttempts = 1)) + + (client.start(rpcUser.username, rpcUser.password, gracefulReconnect = gracefulReconnect)).use { + val rpcOps = it.proxy as ReconnectingCordaRPCOps + rpcOps.setFlowsDrainingModeEnabled(true) + + assertThatThrownBy { rpcOps.startTrackedFlow(::CashIssueFlow, 10.DOLLARS, OpaqueBytes.of(0), defaultNotaryIdentity).returnValue.get() } + .isInstanceOf(RejectedCommandException::class.java).hasMessage("Node is draining before shutdown. Cannot start new flows through RPC.") + } + } + } + + @Test(timeout=300_000) fun `rpc client calls and returned observables continue working when the server crashes and restarts`() { driver(DriverParameters(cordappsForAllNodes = FINANCE_CORDAPPS)) { diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/ReconnectingCordaRPCOps.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/ReconnectingCordaRPCOps.kt index af3c7ab1b5..ff833dd03b 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/ReconnectingCordaRPCOps.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/ReconnectingCordaRPCOps.kt @@ -325,8 +325,8 @@ class ReconnectingCordaRPCOps private constructor( } when (e.targetException) { is RejectedCommandException -> { - log.warn("Node is being shutdown. Operation ${method.name} rejected. Retrying when node is up...", e) - reconnectingRPCConnection.reconnectOnError(e) + log.warn("Node is being shutdown. Operation ${method.name} rejected. Shutting down...", e) + throw e.targetException } is ConnectionFailureException -> { log.warn("Failed to perform operation ${method.name}. Connection dropped. Retrying....", e) From 6bc2c79e235134a95b0a361d2e28eaaea0b24cd3 Mon Sep 17 00:00:00 2001 From: Dan Newton Date: Thu, 2 Jul 2020 16:14:51 +0100 Subject: [PATCH 47/85] NOTICK `NodeBasedTest` take in cordapps (#6424) In enterprise, `AuthDBTests` picked up a schema from a unit test and included it in the cordapp it builds. This schema does not have a migration and therefore fails the integration tests. `NodeBasedTest` now lets cordapps to be defined and passed in to avoid this issue. It defaults to making a cordapp from the tests base directory if none are provided. --- .../corda/client/rpc/CordaRPCJavaClientTest.java | 11 ++++++++++- .../net/corda/client/rpc/CordaRPCClientTest.kt | 3 ++- .../corda/client/rpc/FlowsExecutionModeRpcTest.kt | 4 +--- .../kotlin/net/corda/node/AuthDBTests.kt | 6 +++++- .../corda/testing/node/internal/NodeBasedTest.kt | 13 +++++++++---- 5 files changed, 27 insertions(+), 10 deletions(-) diff --git a/client/rpc/src/integration-test/java/net/corda/client/rpc/CordaRPCJavaClientTest.java b/client/rpc/src/integration-test/java/net/corda/client/rpc/CordaRPCJavaClientTest.java index 89fc874e1b..3f2f2ec680 100644 --- a/client/rpc/src/integration-test/java/net/corda/client/rpc/CordaRPCJavaClientTest.java +++ b/client/rpc/src/integration-test/java/net/corda/client/rpc/CordaRPCJavaClientTest.java @@ -13,6 +13,7 @@ import net.corda.node.internal.NodeWithInfo; import net.corda.testing.internal.InternalTestUtilsKt; import net.corda.testing.node.User; import net.corda.testing.node.internal.NodeBasedTest; +import net.corda.testing.node.internal.TestCordappInternal; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -30,10 +31,18 @@ import static net.corda.node.services.Permissions.invokeRpc; import static net.corda.node.services.Permissions.startFlow; import static net.corda.testing.core.TestConstants.ALICE_NAME; import static net.corda.testing.core.TestConstants.DUMMY_NOTARY_NAME; +import static net.corda.testing.node.internal.InternalTestUtilsKt.FINANCE_CORDAPPS; +import static net.corda.testing.node.internal.InternalTestUtilsKt.cordappWithPackages; public class CordaRPCJavaClientTest extends NodeBasedTest { public CordaRPCJavaClientTest() { - super(Arrays.asList("net.corda.finance.contracts", CashSchemaV1.class.getPackage().getName()), Collections.singletonList(DUMMY_NOTARY_NAME)); + super(cordapps(), Collections.singletonList(DUMMY_NOTARY_NAME)); + } + + private static Set cordapps() { + Set cordapps = new HashSet<>(FINANCE_CORDAPPS); + cordapps.add(cordappWithPackages(CashSchemaV1.class.getPackage().getName())); + return cordapps; } private List perms = Arrays.asList( diff --git a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt index 59f05cb8e2..1ebd6f29b5 100644 --- a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt +++ b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt @@ -40,6 +40,7 @@ import net.corda.testing.core.expect import net.corda.testing.core.expectEvents import net.corda.testing.core.sequence import net.corda.testing.node.User +import net.corda.testing.node.internal.FINANCE_CORDAPPS import net.corda.testing.node.internal.NodeBasedTest import net.corda.testing.node.internal.ProcessUtilities import net.corda.testing.node.internal.poll @@ -62,7 +63,7 @@ import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue -class CordaRPCClientTest : NodeBasedTest(listOf("net.corda.finance"), notaries = listOf(DUMMY_NOTARY_NAME)) { +class CordaRPCClientTest : NodeBasedTest(FINANCE_CORDAPPS, notaries = listOf(DUMMY_NOTARY_NAME)) { companion object { val rpcUser = User("user1", "test", permissions = setOf(all())) val log = contextLogger() diff --git a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/FlowsExecutionModeRpcTest.kt b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/FlowsExecutionModeRpcTest.kt index 8084484735..22b11cee50 100644 --- a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/FlowsExecutionModeRpcTest.kt +++ b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/FlowsExecutionModeRpcTest.kt @@ -2,9 +2,7 @@ package net.corda.client.rpc import net.corda.core.context.Actor import net.corda.core.context.Trace -import net.corda.core.internal.packageName import net.corda.core.messaging.CordaRPCOps -import net.corda.finance.schemas.CashSchemaV1 import net.corda.node.internal.NodeWithInfo import net.corda.node.services.Permissions import net.corda.testing.core.ALICE_NAME @@ -14,7 +12,7 @@ import org.assertj.core.api.Assertions.assertThat import org.junit.Before import org.junit.Test -class FlowsExecutionModeTests : NodeBasedTest(emptyList()) { +class FlowsExecutionModeTests : NodeBasedTest() { private val rpcUser = User("user1", "test", permissions = setOf(Permissions.all())) private lateinit var node: NodeWithInfo diff --git a/node/src/integration-test/kotlin/net/corda/node/AuthDBTests.kt b/node/src/integration-test/kotlin/net/corda/node/AuthDBTests.kt index 70293b6074..e2795b83b3 100644 --- a/node/src/integration-test/kotlin/net/corda/node/AuthDBTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/AuthDBTests.kt @@ -15,6 +15,7 @@ import net.corda.node.services.Permissions import net.corda.node.services.config.PasswordEncryption import net.corda.testing.core.ALICE_NAME import net.corda.testing.node.internal.NodeBasedTest +import net.corda.testing.node.internal.cordappForClasses import org.apache.activemq.artemis.api.core.ActiveMQSecurityException import org.apache.shiro.authc.credential.DefaultPasswordService import org.junit.After @@ -32,7 +33,7 @@ import kotlin.test.assertFailsWith * check authentication/authorization of RPC connections. */ @RunWith(Parameterized::class) -class AuthDBTests : NodeBasedTest() { +class AuthDBTests : NodeBasedTest(cordappPackages = CORDAPPS) { private lateinit var node: NodeWithInfo private lateinit var client: CordaRPCClient private lateinit var db: UsersDB @@ -43,6 +44,9 @@ class AuthDBTests : NodeBasedTest() { @JvmStatic @Parameterized.Parameters(name = "password encryption format = {0}") fun encFormats() = arrayOf(PasswordEncryption.NONE, PasswordEncryption.SHIRO_1_CRYPT) + + @Suppress("SpreadOperator") + private val CORDAPPS = setOf(cordappForClasses(*AuthDBTests::class.nestedClasses.map { it.java }.toTypedArray())) } @Suppress("MemberVisibilityCanBePrivate") diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt index 662766665b..e24c82e982 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt @@ -39,9 +39,10 @@ import kotlin.test.assertFalse // TODO Some of the logic here duplicates what's in the driver - the reason why it's not straightforward to replace it by // using DriverDSLImpl in `init()` and `stopAllNodes()` is because of the platform version passed to nodes (driver doesn't // support this, and it's a property of the Corda JAR) -abstract class NodeBasedTest -@JvmOverloads -constructor(private val cordappPackages: List = emptyList(), private val notaries: List = emptyList()) { +abstract class NodeBasedTest @JvmOverloads constructor( + private val cordappPackages: Set = emptySet(), + private val notaries: List = emptyList() +) { companion object { private val WHITESPACE = "\\s++".toRegex() } @@ -120,7 +121,11 @@ constructor(private val cordappPackages: List = emptyList(), private val ) + configOverrides ) - val customCordapps = cordappsForPackages(getCallerPackage(NodeBasedTest::class)?.let { cordappPackages + it } ?: cordappPackages) + val customCordapps = if (cordappPackages.isNotEmpty()) { + cordappPackages + } else { + cordappsForPackages(getCallerPackage(NodeBasedTest::class)?.let { listOf(it) } ?: emptyList()) + } TestCordappInternal.installCordapps(baseDirectory, emptySet(), customCordapps) val parsedConfig = config.parseAsNodeConfiguration().value() From f8d86c06174667d990071e780f0132e172cf32b2 Mon Sep 17 00:00:00 2001 From: Jonathan Locke <36930160+lockathan@users.noreply.github.com> Date: Thu, 2 Jul 2020 16:17:34 +0100 Subject: [PATCH 48/85] INFRA-415: Add empty javadoc to node-driver (#6429) Maven central will not allow the node-driver to be published without a Javadoc `.jar` file, even if it is empty. This adds an empty Javadoc `.jar` to the output. --- testing/node-driver/README.md | 2 ++ testing/node-driver/build.gradle | 12 ++++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 testing/node-driver/README.md diff --git a/testing/node-driver/README.md b/testing/node-driver/README.md new file mode 100644 index 0000000000..0ae850af61 --- /dev/null +++ b/testing/node-driver/README.md @@ -0,0 +1,2 @@ +## corda-node-driver. +This artifact is the node-driver used for testing Corda. diff --git a/testing/node-driver/build.gradle b/testing/node-driver/build.gradle index c632949b1a..7f3b3be7ee 100644 --- a/testing/node-driver/build.gradle +++ b/testing/node-driver/build.gradle @@ -73,9 +73,17 @@ jar { } } + +tasks.named('javadocJar', Jar) { + from 'README.md' + include 'README.md' +} + +tasks.named('javadoc', Javadoc) { + enabled = false +} + publish { - publishSources = true - publishJavadoc = false name jar.baseName } From a2058490ed81d2c4597774604886fe97c5d94045 Mon Sep 17 00:00:00 2001 From: Viktor Kolomeyko Date: Fri, 3 Jul 2020 09:29:43 +0100 Subject: [PATCH 49/85] CORDA-3880: Streamline re-connection logic in RPCClientProxyHandler (#6426) * CORDA-3880: Streamline re-connection logic in RPCClientProxyHandler * CORDA-3880: Address PR review comments from @dimosr * CORDA-3880: Explicitly mention contract around `maxReconnectAttempts` * CORDA-3880: Handle `maxReconnectAttempts = 0` and do not re-connect --- .../net/corda/client/rpc/CordaRPCClient.kt | 3 +- .../rpc/internal/RPCClientProxyHandler.kt | 64 +++++++++++-------- 2 files changed, 39 insertions(+), 28 deletions(-) diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt index 6308ae1ebf..bf858c9290 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt @@ -158,7 +158,8 @@ open class CordaRPCClientConfiguration @JvmOverloads constructor( open val connectionRetryIntervalMultiplier: Double = 1.5, /** - * Maximum reconnect attempts on failover or disconnection. The default is -1 which means unlimited. + * Maximum reconnect attempts on failover or disconnection. + * Any negative value would mean that there will be an infinite number of reconnect attempts. */ open val maxReconnectAttempts: Int = unlimitedReconnectAttempts, diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt index 9dc7d5cc70..88c059ff9b 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt @@ -76,10 +76,10 @@ import kotlin.reflect.jvm.javaMethod * forwarded to the [UnicastSubject]. Note that the observations themselves may contain further [Observable]s, which are * handled in the same way. * - * To do the above we take advantage of Kryo's datastructure traversal. When the client is deserialising a message from - * the server that may contain Observables it is supplied with an [ObservableContext] that exposes the map used to demux - * the observations. When an [Observable] is encountered during traversal a new [UnicastSubject] is added to the map and - * we carry on. Each observation later contains the corresponding Observable ID, and we just forward that to the + * To do the above we take advantage of serialisation data structure traversal. When the client is deserialising a message from + * the server that may contain [Observable]s, it is supplied with an [ObservableContext] that exposes the map used to demux + * the observations. When a new [Observable] is encountered during traversal a new [UnicastSubject] is added to the map and + * we carry on. Each observation later contains the corresponding [Observable] ID, and we just forward that to the * associated [UnicastSubject]. * * The client may signal that it no longer consumes a particular [Observable]. This may be done explicitly by @@ -88,12 +88,12 @@ import kotlin.reflect.jvm.javaMethod * The cleanup happens in batches using a dedicated reaper, scheduled on [reaperExecutor]. * * The client will attempt to failover in case the server become unreachable. Depending on the [ServerLocator] instance - * passed in the constructor, failover is either handle at Artemis level or client level. If only one transport + * passed in the constructor, failover is either handled at Artemis level or client level. If only one transport * was used to create the [ServerLocator], failover is handled by Artemis (retrying based on [CordaRPCClientConfiguration]. * If a list of transport configurations was used, failover is handled locally. Artemis is able to do it, however the * brokers on server side need to be configured in HA mode and the [ServerLocator] needs to be created with HA as well. */ -class RPCClientProxyHandler( +internal class RPCClientProxyHandler( private val rpcConfiguration: CordaRPCClientConfiguration, private val rpcUsername: String, private val rpcPassword: String, @@ -247,7 +247,7 @@ class RPCClientProxyHandler( try { sessionFactory = serverLocator.createSessionFactory() } catch (e: ActiveMQNotConnectedException) { - throw (RPCException("Cannot connect to server(s). Tried with all available servers.", e)) + throw RPCException("Cannot connect to server(s). Tried with all available servers.", e) } // Depending on how the client is constructed, connection failure is treated differently if (serverLocator.staticTransportConfigurations.size == 1) { @@ -380,9 +380,11 @@ class RPCClientProxyHandler( is RPCApi.ServerToClient.Observation -> { val observable: UnicastSubject>? = observableContext.observableMap.getIfPresent(serverToClient.id) if (observable == null) { - log.debug("Observation ${serverToClient.content} arrived to unknown Observable with ID ${serverToClient.id}. " + - "This may be due to an observation arriving before the server was " + - "notified of observable shutdown") + log.debug { + "Observation ${serverToClient.content} arrived to unknown Observable with ID ${serverToClient.id}. " + + "This may be due to an observation arriving before the server was " + + "notified of observable shutdown" + } } else { // We schedule the onNext() on an executor sticky-pooled based on the Observable ID. observationExecutorPool.run(serverToClient.id) { executor -> @@ -461,7 +463,7 @@ class RPCClientProxyHandler( } } observableContext.observableMap.invalidateAll() - rpcReplyMap.forEach { _, replyFuture -> + rpcReplyMap.forEach { (_, replyFuture) -> replyFuture.setException(ConnectionFailureException()) } @@ -528,23 +530,26 @@ class RPCClientProxyHandler( } private fun attemptReconnect() { - var reconnectAttempts = rpcConfiguration.maxReconnectAttempts.times(serverLocator.staticTransportConfigurations.size) + // This can be a negative number as `rpcConfiguration.maxReconnectAttempts = -1` means infinite number of re-connects + val maxReconnectCount = rpcConfiguration.maxReconnectAttempts.times(serverLocator.staticTransportConfigurations.size) + log.debug { "maxReconnectCount = $maxReconnectCount" } + var reconnectAttempt = 1 var retryInterval = rpcConfiguration.connectionRetryInterval val maxRetryInterval = rpcConfiguration.connectionMaxRetryInterval - var transportIterator = serverLocator.staticTransportConfigurations.iterator() - while (transportIterator.hasNext() && reconnectAttempts != 0) { - val transport = transportIterator.next() - if (!transportIterator.hasNext()) - transportIterator = serverLocator.staticTransportConfigurations.iterator() + fun shouldRetry(reconnectAttempt: Int) = + if (maxReconnectCount < 0) true else reconnectAttempt <= maxReconnectCount - log.debug("Trying to connect using ${transport.params}") + while (shouldRetry(reconnectAttempt)) { + val transport = serverLocator.staticTransportConfigurations.let { it[(reconnectAttempt - 1) % it.size] } + + log.debug { "Trying to connect using ${transport.params}" } try { if (!serverLocator.isClosed) { sessionFactory = serverLocator.createSessionFactory(transport) } else { log.warn("Stopping reconnect attempts.") - log.debug("Server locator is closed or garbage collected. Proxy may have been closed during reconnect.") + log.debug { "Server locator is closed or garbage collected. Proxy may have been closed during reconnect." } break } } catch (e: ActiveMQException) { @@ -552,12 +557,12 @@ class RPCClientProxyHandler( Thread.sleep(retryInterval.toMillis()) } catch (e: InterruptedException) {} // Could not connect, try with next server transport. - reconnectAttempts-- + reconnectAttempt++ retryInterval = minOf(maxRetryInterval, retryInterval.times(rpcConfiguration.connectionRetryIntervalMultiplier.toLong())) continue } - log.debug("Connected successfully after $reconnectAttempts attempts using ${transport.params}.") + log.debug { "Connected successfully after $reconnectAttempt attempts using ${transport.params}." } log.info("RPC server available.") sessionFactory!!.addFailoverListener(this::haFailoverHandler) initSessions() @@ -566,8 +571,12 @@ class RPCClientProxyHandler( break } - if (reconnectAttempts == 0 || sessionFactory == null) - log.error("Could not reconnect to the RPC server.") + val maxReconnectReached = !shouldRetry(reconnectAttempt) + if (maxReconnectReached || sessionFactory == null) { + val errMessage = "Could not reconnect to the RPC server after trying $reconnectAttempt times." + + if (sessionFactory != null) "" else " It was never possible to to establish connection with any of the endpoints." + log.error(errMessage) + } } private fun initSessions() { @@ -620,10 +629,11 @@ class RPCClientProxyHandler( sendingEnabled.set(false) log.warn("Terminating observables.") val m = observableContext.observableMap.asMap() + val connectionFailureException = ConnectionFailureException() m.keys.forEach { k -> observationExecutorPool.run(k) { try { - m[k]?.onError(ConnectionFailureException()) + m[k]?.onError(connectionFailureException) } catch (e: Exception) { log.error("Unexpected exception when RPC connection failure handling", e) } @@ -631,8 +641,8 @@ class RPCClientProxyHandler( } observableContext.observableMap.invalidateAll() - rpcReplyMap.forEach { _, replyFuture -> - replyFuture.setException(ConnectionFailureException()) + rpcReplyMap.forEach { (_, replyFuture) -> + replyFuture.setException(connectionFailureException) } rpcReplyMap.clear() @@ -666,5 +676,5 @@ class RPCClientProxyHandler( } } -private typealias RpcReplyMap = ConcurrentHashMap> +private typealias RpcReplyMap = ConcurrentHashMap> From 6aa19723e6f9604f598cd5089dd05a2b8929d61a Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Fri, 3 Jul 2020 19:42:29 +0000 Subject: [PATCH 50/85] INFRA-417 Improve driver DSL test stability (#6415) * Move log messages that are not useful in typical usage from info to debug level to reduce log spam. * Add node startup check before attempting to connect. --- .../distributed/DistributedServiceTests.kt | 6 ++++-- .../kotlin/net/corda/testing/core/Expect.kt | 4 ++-- .../testing/node/internal/DriverDSLImpl.kt | 19 +++++++++++++++---- .../node/internal/InternalTestUtils.kt | 4 ++++ 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/node/src/integration-test-slow/kotlin/net/corda/node/services/distributed/DistributedServiceTests.kt b/node/src/integration-test-slow/kotlin/net/corda/node/services/distributed/DistributedServiceTests.kt index 55dcbcfc9f..ad04a89d8c 100644 --- a/node/src/integration-test-slow/kotlin/net/corda/node/services/distributed/DistributedServiceTests.kt +++ b/node/src/integration-test-slow/kotlin/net/corda/node/services/distributed/DistributedServiceTests.kt @@ -29,6 +29,8 @@ import org.junit.Ignore import org.junit.Test import rx.Observable import java.util.* +import kotlin.test.assertEquals +import kotlin.test.assertTrue class DistributedServiceTests { private lateinit var alice: NodeHandle @@ -157,9 +159,9 @@ class DistributedServiceTests { // The distribution of requests should be very close to sg like 16/17/17 as by default artemis does round robin println("Notarisation distribution: $notarisationsPerNotary") - require(notarisationsPerNotary.size == 3) + assertEquals(3, notarisationsPerNotary.size) // We allow some leeway for artemis as it doesn't always produce perfect distribution - require(notarisationsPerNotary.values.all { it > 10 }) + assertTrue { notarisationsPerNotary.values.all { it > 10 } } } private fun issueCash(amount: Amount) { diff --git a/testing/core-test-utils/src/main/kotlin/net/corda/testing/core/Expect.kt b/testing/core-test-utils/src/main/kotlin/net/corda/testing/core/Expect.kt index af961ba1a2..8a4d7d282a 100644 --- a/testing/core-test-utils/src/main/kotlin/net/corda/testing/core/Expect.kt +++ b/testing/core-test-utils/src/main/kotlin/net/corda/testing/core/Expect.kt @@ -164,14 +164,14 @@ fun S.genericExpectEvents( } val next = state.nextState(event) val expectedStates = state.getExpectedEvents() - log.info("$event :: ${expectedStates.map { it.simpleName }} -> ${next?.second?.getExpectedEvents()?.map { it.simpleName }}") + log.debug("$event :: ${expectedStates.map { it.simpleName }} -> ${next?.second?.getExpectedEvents()?.map { it.simpleName }}") if (next == null) { val message = "Got $event, did not match any expectations of type ${expectedStates.map { it.simpleName }}" if (isStrict) { finishFuture.setException(Exception(message)) state = ExpectComposeState.Finished() } else { - log.info("$message, discarding event as isStrict=false") + log.debug("$message, discarding event as isStrict=false") } } else { state = next.second diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt index 56e5b703da..ee55c9999c 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt @@ -9,6 +9,7 @@ import com.typesafe.config.ConfigRenderOptions import com.typesafe.config.ConfigValue import com.typesafe.config.ConfigValueFactory import net.corda.client.rpc.CordaRPCClient +import net.corda.client.rpc.RPCException import net.corda.cliutils.CommonCliConstants.BASE_DIR import net.corda.core.concurrent.CordaFuture import net.corda.core.concurrent.firstOf @@ -218,16 +219,24 @@ class DriverDSLImpl( } } - private fun establishRpc(config: NodeConfig, processDeathFuture: CordaFuture): CordaFuture { + /** + * @param pollInterval the interval to wait between attempting to connect, if + * a connection attempt fails. + */ + private fun establishRpc(config: NodeConfig, + processDeathFuture: CordaFuture): CordaFuture { val rpcAddress = config.corda.rpcOptions.address val clientRpcSslOptions = clientSslOptionsCompatibleWith(config.corda.rpcOptions) val client = CordaRPCClient(rpcAddress, sslConfiguration = clientRpcSslOptions) - val connectionFuture = poll(executorService, "RPC connection") { + val connectionFuture = poll( + executorService = executorService, + pollName = "RPC connection", + pollInterval = RPC_CONNECT_POLL_INTERVAL) { try { config.corda.rpcUsers[0].run { client.start(username, password) } - } catch (e: Exception) { + } catch (e: RPCException) { if (processDeathFuture.isDone) throw e - log.info("Exception while connecting to RPC, retrying to connect at $rpcAddress", e) + log.info("Failed to connect to RPC at $rpcAddress") null } } @@ -673,6 +682,7 @@ class DriverDSLImpl( } ) val nodeFuture: CordaFuture = nodeAndThreadFuture.flatMap { (node, thread) -> + node.node.nodeReadyFuture.get() // Wait for the node to be ready before we connect to the node establishRpc(config, openFuture()).flatMap { rpc -> visibilityHandle.listen(rpc).map { InProcessImpl(rpc.nodeInfo(), rpc, config.corda, webAddress, useHTTPS, thread, onNodeExit, node) @@ -779,6 +789,7 @@ class DriverDSLImpl( } companion object { + private val RPC_CONNECT_POLL_INTERVAL: Duration = 100.millis internal val log = contextLogger() // While starting with inProcess mode, we need to have different names to avoid clashes diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalTestUtils.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalTestUtils.kt index 1f4b3ab632..67bfb8e1b8 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalTestUtils.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalTestUtils.kt @@ -210,6 +210,10 @@ fun addressMustNotBeBoundFuture(executorService: ScheduledExecutorService, hostA } } +/** + * @param pollInterval the interval running the background task. + * @param warnCount number of iterations to poll before printing a warning message. + */ fun poll( executorService: ScheduledExecutorService, pollName: String, From 0d5bed52439ca50ae1277dbb9a8c01a48d684658 Mon Sep 17 00:00:00 2001 From: Ryan Fowler Date: Mon, 6 Jul 2020 11:42:36 +0100 Subject: [PATCH 51/85] ENT-5131: Avoid NPE by throwing a catchable exception when openAttachment fails. (#6408) --- .../kotlin/net/corda/core/messaging/CordaRPCOps.kt | 7 ++++++- .../net/corda/node/internal/CordaRPCOpsImpl.kt | 4 +++- .../kotlin/net/corda/node/CordaRPCOpsImplTest.kt | 13 +++++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt index a63f6013e7..6098b0c707 100644 --- a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt +++ b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt @@ -302,7 +302,12 @@ interface CordaRPCOps : RPCOps { /** Checks whether an attachment with the given hash is stored on the node. */ fun attachmentExists(id: SecureHash): Boolean - /** Download an attachment JAR by ID. */ + /** + * Download an attachment JAR by ID. + * @param id the id of the attachment to open + * @return the stream of the JAR + * @throws RPCException if the attachment doesn't exist + * */ fun openAttachment(id: SecureHash): InputStream /** Uploads a jar to the node, returns it's hash. */ diff --git a/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt b/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt index 571c97b82c..6d058aaf37 100644 --- a/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt +++ b/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt @@ -1,5 +1,6 @@ package net.corda.node.internal +import net.corda.client.rpc.RPCException import net.corda.client.rpc.notUsed import net.corda.common.logging.CordaVersion import net.corda.core.CordaRuntimeException @@ -263,7 +264,8 @@ internal class CordaRPCOpsImpl( } override fun openAttachment(id: SecureHash): InputStream { - return services.attachments.openAttachment(id)!!.open() + return services.attachments.openAttachment(id)?.open() ?: + throw RPCException("Unable to open attachment with id: $id") } override fun uploadAttachment(jar: InputStream): SecureHash { diff --git a/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt b/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt index dcd4706019..d7b45e42e6 100644 --- a/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt +++ b/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt @@ -2,11 +2,13 @@ package net.corda.node import co.paralleluniverse.fibers.Suspendable import net.corda.client.rpc.PermissionException +import net.corda.client.rpc.RPCException import net.corda.core.context.AuthServiceId import net.corda.core.context.InvocationContext import net.corda.core.contracts.Amount import net.corda.core.contracts.ContractState import net.corda.core.contracts.Issued +import net.corda.core.crypto.SecureHash import net.corda.core.crypto.isFulfilledBy import net.corda.core.crypto.keys import net.corda.core.flows.FlowLogic @@ -353,6 +355,17 @@ class CordaRPCOpsImplTest { } } + @Test(timeout=300_000) + fun `trying to open attachment which doesnt exist throws error`() { + CURRENT_RPC_CONTEXT.set(RpcAuthContext(InvocationContext.rpc(testActor()), buildSubject("TEST_USER", emptySet()))) + withPermissions(invokeRpc(CordaRPCOps::openAttachment)) { + assertThatThrownBy { + rpc.openAttachment(SecureHash.zeroHash) + }.isInstanceOf(RPCException::class.java) + .withFailMessage("Unable to open attachment with id: ${SecureHash.zeroHash}") + } + } + @Test(timeout=300_000) fun `attachment uploaded with metadata has specified uploader`() { CURRENT_RPC_CONTEXT.set(RpcAuthContext(InvocationContext.rpc(testActor()), buildSubject("TEST_USER", emptySet()))) From a1e1bf4e6d920d6943539e1675478d39c1004e8a Mon Sep 17 00:00:00 2001 From: Dan Newton Date: Mon, 6 Jul 2020 22:43:48 +0100 Subject: [PATCH 52/85] CORDA-3848 Uncaught exception hospitalises flow (#6377) When an uncaught exception propagates all the way to the flow exception handler, the flow will be forced into observation/hospitalised. The updating of the checkpoints status is done on a separate thread as the fiber cannot be relied on anymore. The new thread is needed to allow database transaction to be created and committed. Failures to the status update will be rescheduled to ensure that this information is eventually reflected in the database. --- .../StatemachineErrorHandlingTest.kt | 14 ++ .../StatemachineGeneralErrorHandlingTest.kt | 188 ++++++++++++++++++ .../node/services/api/CheckpointStorage.kt | 2 + .../persistence/DBCheckpointStorage.kt | 5 + .../FlowDefaultUncaughtExceptionHandler.kt | 67 +++++++ .../SingleThreadedStateMachineManager.kt | 25 ++- .../statemachine/StaffedFlowHospital.kt | 24 +-- .../persistence/DBCheckpointStorageTests.kt | 18 ++ 8 files changed, 321 insertions(+), 22 deletions(-) create mode 100644 node/src/main/kotlin/net/corda/node/services/statemachine/FlowDefaultUncaughtExceptionHandler.kt diff --git a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineErrorHandlingTest.kt b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineErrorHandlingTest.kt index d27a71430a..6716b0baba 100644 --- a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineErrorHandlingTest.kt +++ b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineErrorHandlingTest.kt @@ -109,6 +109,20 @@ abstract class StatemachineErrorHandlingTest { } } + @StartableByRPC + class ThrowAnErrorFlow : FlowLogic() { + @Suspendable + override fun call(): String { + throwException() + return "cant get here" + } + + private fun throwException() { + logger.info("Throwing exception in flow") + throw IllegalStateException("throwing exception in flow") + } + } + @StartableByRPC class GetNumberOfUncompletedCheckpointsFlow : FlowLogic() { override fun call(): Long { diff --git a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineGeneralErrorHandlingTest.kt b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineGeneralErrorHandlingTest.kt index 6040df4ed9..6ff6bdacbc 100644 --- a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineGeneralErrorHandlingTest.kt +++ b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineGeneralErrorHandlingTest.kt @@ -1715,4 +1715,192 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { assertEquals(2, charlieClient.startFlow(StatemachineErrorHandlingTest::GetNumberOfUncompletedCheckpointsFlow).returnValue.get()) } } + + /** + * Throws an exception when calling [FlowStateMachineImpl.recordDuration] to cause an unexpected error during flow initialisation. + * + * The hospital has the flow's medical history updated with the new failure added to it. As the failure occurred before the original + * checkpoint was persisted, there is no checkpoint to update in the database. + */ + @Test(timeout = 300_000) + fun `unexpected error during flow initialisation that gets caught by default exception handler puts flow into in-memory overnight observation`() { + startDriver { + val charlie = createNode(CHARLIE_NAME) + val alice = createBytemanNode(ALICE_NAME) + val rules = """ + RULE Create Counter + CLASS ${FlowStateMachineImpl::class.java.name} + METHOD recordDuration + AT ENTRY + IF createCounter("counter", $counter) + DO traceln("Counter created") + ENDRULE + + RULE Throw exception + CLASS ${FlowStateMachineImpl::class.java.name} + METHOD openThreadLocalWormhole + AT ENTRY + IF readCounter("counter") < 1 + DO incrementCounter("counter"); traceln("Throwing exception"); throw new java.lang.RuntimeException("die dammit die") + ENDRULE + """.trimIndent() + + submitBytemanRules(rules) + + executor.execute { + alice.rpc.startFlow( + StatemachineErrorHandlingTest::SendAMessageFlow, + charlie.nodeInfo.singleIdentity() + ) + } + + Thread.sleep(10.seconds.toMillis()) + + val (discharge, observation) = alice.rpc.startFlow(StatemachineErrorHandlingTest::GetHospitalCountersFlow).returnValue.get() + assertEquals(0, discharge) + assertEquals(1, observation) + assertEquals(1, alice.rpc.stateMachinesSnapshot().size) + // The flow failed during flow initialisation before committing the original checkpoint + // therefore there is no checkpoint to update the status of + assertEquals(0, alice.rpc.startFlow(StatemachineErrorHandlingTest::GetNumberOfHospitalizedCheckpointsFlow).returnValue.get()) + } + } + + /** + * Throws an exception when calling [FlowStateMachineImpl.logFlowError] to cause an unexpected error after the flow has properly + * initialised. + * + * The hospital has the flow's medical history updated with the new failure added to it. The status of the checkpoint is also set to + * [Checkpoint.FlowStatus.HOSPITALIZED] to reflect this information in the database. + */ + @Test(timeout = 300_000) + fun `unexpected error after flow initialisation that gets caught by default exception handler puts flow into overnight observation and reflected in database`() { + startDriver { + val alice = createBytemanNode(ALICE_NAME) + val rules = """ + RULE Create Counter + CLASS ${FlowStateMachineImpl::class.java.name} + METHOD recordDuration + AT ENTRY + IF createCounter("counter", $counter) + DO traceln("Counter created") + ENDRULE + + RULE Throw exception + CLASS ${FlowStateMachineImpl::class.java.name} + METHOD logFlowError + AT ENTRY + IF readCounter("counter") < 1 + DO incrementCounter("counter"); traceln("Throwing exception"); throw new java.lang.RuntimeException("die dammit die") + ENDRULE + """.trimIndent() + + submitBytemanRules(rules) + + assertFailsWith { + alice.rpc.startFlow(StatemachineErrorHandlingTest::ThrowAnErrorFlow).returnValue.getOrThrow(30.seconds) + } + + val (discharge, observation) = alice.rpc.startFlow(StatemachineErrorHandlingTest::GetHospitalCountersFlow).returnValue.get() + assertEquals(0, discharge) + assertEquals(1, observation) + assertEquals(1, alice.rpc.stateMachinesSnapshot().size) + assertEquals(1, alice.rpc.startFlow(StatemachineErrorHandlingTest::GetNumberOfHospitalizedCheckpointsFlow).returnValue.get()) + } + } + + /** + * Throws an exception when calling [FlowStateMachineImpl.logFlowError] to cause an unexpected error after the flow has properly + * initialised. When updating the status of the flow to [Checkpoint.FlowStatus.HOSPITALIZED] an error occurs. + * + * The update is rescheduled and tried again. This is done separate from the fiber. + */ + @Test(timeout = 300_000) + fun `unexpected error after flow initialisation that gets caught by default exception handler retries the status update if it fails`() { + startDriver { + val alice = createBytemanNode(ALICE_NAME) + val rules = """ + RULE Create Counter + CLASS ${FlowStateMachineImpl::class.java.name} + METHOD recordDuration + AT ENTRY + IF createCounter("counter", $counter) + DO traceln("Counter created") + ENDRULE + + RULE Throw exception + CLASS ${FlowStateMachineImpl::class.java.name} + METHOD logFlowError + AT ENTRY + IF readCounter("counter") < 1 + DO incrementCounter("counter"); traceln("Throwing exception"); throw new java.lang.RuntimeException("die dammit die") + ENDRULE + + RULE Throw exception when updating status + INTERFACE ${CheckpointStorage::class.java.name} + METHOD updateStatus + AT ENTRY + IF readCounter("counter") < 2 + DO incrementCounter("counter"); traceln("Throwing exception"); throw new java.lang.RuntimeException("should be a sql exception") + ENDRULE + """.trimIndent() + + submitBytemanRules(rules) + + assertFailsWith { + alice.rpc.startFlow(StatemachineErrorHandlingTest::ThrowAnErrorFlow).returnValue.getOrThrow(50.seconds) + } + + val (discharge, observation) = alice.rpc.startFlow(StatemachineErrorHandlingTest::GetHospitalCountersFlow).returnValue.get() + assertEquals(0, discharge) + assertEquals(1, observation) + assertEquals(1, alice.rpc.stateMachinesSnapshot().size) + assertEquals(1, alice.rpc.startFlow(StatemachineErrorHandlingTest::GetNumberOfHospitalizedCheckpointsFlow).returnValue.get()) + } + } + + /** + * Throws an exception when calling [FlowStateMachineImpl.recordDuration] to cause an unexpected error after a flow has returned its + * result to the client. + * + * As the flow has already returned its result to the client, then the status of the flow has already been updated correctly and now the + * flow has experienced an unexpected error. There is no need to change the status as the flow has already finished. + */ + @Test(timeout = 300_000) + fun `unexpected error after flow has returned result to client that gets caught by default exception handler does nothing except log`() { + startDriver { + val charlie = createNode(CHARLIE_NAME) + val alice = createBytemanNode(ALICE_NAME) + val rules = """ + RULE Create Counter + CLASS ${FlowStateMachineImpl::class.java.name} + METHOD recordDuration + AT ENTRY + IF createCounter("counter", $counter) + DO traceln("Counter created") + ENDRULE + + RULE Throw exception + CLASS ${FlowStateMachineImpl::class.java.name} + METHOD recordDuration + AT ENTRY + IF readCounter("counter") < 1 + DO incrementCounter("counter"); traceln("Throwing exception"); throw new java.lang.RuntimeException("die dammit die") + ENDRULE + """.trimIndent() + + submitBytemanRules(rules) + + alice.rpc.startFlow( + StatemachineErrorHandlingTest::SendAMessageFlow, + charlie.nodeInfo.singleIdentity() + ).returnValue.getOrThrow(30.seconds) + + val (discharge, observation) = alice.rpc.startFlow(StatemachineErrorHandlingTest::GetHospitalCountersFlow).returnValue.get() + assertEquals(0, discharge) + assertEquals(0, observation) + assertEquals(0, alice.rpc.stateMachinesSnapshot().size) + assertEquals(0, alice.rpc.startFlow(StatemachineErrorHandlingTest::GetNumberOfHospitalizedCheckpointsFlow).returnValue.get()) + } + } } \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/api/CheckpointStorage.kt b/node/src/main/kotlin/net/corda/node/services/api/CheckpointStorage.kt index 0bac15c171..1edc2491a8 100644 --- a/node/src/main/kotlin/net/corda/node/services/api/CheckpointStorage.kt +++ b/node/src/main/kotlin/net/corda/node/services/api/CheckpointStorage.kt @@ -65,4 +65,6 @@ interface CheckpointStorage { * This method does not fetch [Checkpoint.Serialized.serializedFlowState] to save memory. */ fun getPausedCheckpoints(): Stream> + + fun updateStatus(runId: StateMachineRunId, flowStatus: Checkpoint.FlowStatus) } diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBCheckpointStorage.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBCheckpointStorage.kt index af5778868f..a9072afdab 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBCheckpointStorage.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBCheckpointStorage.kt @@ -499,6 +499,11 @@ class DBCheckpointStorage( } } + override fun updateStatus(runId: StateMachineRunId, flowStatus: FlowStatus) { + val update = "Update ${NODE_DATABASE_PREFIX}checkpoints set status = ${flowStatus.ordinal} where flow_id = '${runId.uuid}'" + currentDBSession().createNativeQuery(update).executeUpdate() + } + private fun createDBFlowMetadata(flowId: String, checkpoint: Checkpoint): DBFlowMetadata { val context = checkpoint.checkpointState.invocationContext val flowInfo = checkpoint.checkpointState.subFlowStack.first() diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowDefaultUncaughtExceptionHandler.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowDefaultUncaughtExceptionHandler.kt new file mode 100644 index 0000000000..0dc5f28791 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowDefaultUncaughtExceptionHandler.kt @@ -0,0 +1,67 @@ +package net.corda.node.services.statemachine + +import co.paralleluniverse.strands.Strand +import net.corda.core.flows.StateMachineRunId +import net.corda.core.utilities.contextLogger +import net.corda.core.utilities.debug +import net.corda.node.services.api.CheckpointStorage +import net.corda.node.utilities.errorAndTerminate +import net.corda.nodeapi.internal.persistence.CordaPersistence +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.TimeUnit + +class FlowDefaultUncaughtExceptionHandler( + private val flowHospital: StaffedFlowHospital, + private val checkpointStorage: CheckpointStorage, + private val database: CordaPersistence, + private val scheduledExecutor: ScheduledExecutorService +) : Strand.UncaughtExceptionHandler { + + private companion object { + val log = contextLogger() + const val RESCHEDULE_DELAY = 30L + } + + override fun uncaughtException(fiber: Strand, throwable: Throwable) { + val id = (fiber as FlowStateMachineImpl<*>).id + if (throwable is VirtualMachineError) { + errorAndTerminate( + "Caught unrecoverable error from flow $id. Forcibly terminating the JVM, this might leave resources open, and most likely will.", + throwable + ) + } else { + fiber.logger.warn("Caught exception from flow $id", throwable) + setFlowToHospitalized(fiber, throwable) + } + } + + private fun setFlowToHospitalized(fiber: FlowStateMachineImpl<*>, throwable: Throwable) { + val id = fiber.id + if (!fiber.resultFuture.isDone) { + fiber.transientState.let { state -> + if (state != null) { + fiber.logger.warn("Forcing flow $id into overnight observation") + flowHospital.forceIntoOvernightObservation(state.value, listOf(throwable)) + val hospitalizedCheckpoint = state.value.checkpoint.copy(status = Checkpoint.FlowStatus.HOSPITALIZED) + val hospitalizedState = state.value.copy(checkpoint = hospitalizedCheckpoint) + fiber.transientState = TransientReference(hospitalizedState) + } else { + fiber.logger.warn("The fiber's transient state is not set, cannot force flow $id into in-memory overnight observation, status will still be updated in database") + } + } + scheduledExecutor.schedule({ setFlowToHospitalizedRescheduleOnFailure(id) }, 0, TimeUnit.SECONDS) + } + } + + @Suppress("TooGenericExceptionCaught") + private fun setFlowToHospitalizedRescheduleOnFailure(id: StateMachineRunId) { + try { + log.debug { "Updating the status of flow $id to hospitalized after uncaught exception" } + database.transaction { checkpointStorage.updateStatus(id, Checkpoint.FlowStatus.HOSPITALIZED) } + log.debug { "Updated the status of flow $id to hospitalized after uncaught exception" } + } catch (e: Exception) { + log.info("Failed to update the status of flow $id to hospitalized after uncaught exception, rescheduling", e) + scheduledExecutor.schedule({ setFlowToHospitalizedRescheduleOnFailure(id) }, RESCHEDULE_DELAY, TimeUnit.SECONDS) + } + } +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/SingleThreadedStateMachineManager.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/SingleThreadedStateMachineManager.kt index 3c7a7dd27f..3082325b63 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/SingleThreadedStateMachineManager.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/SingleThreadedStateMachineManager.kt @@ -38,7 +38,6 @@ import net.corda.node.services.statemachine.interceptors.FiberDeserializationChe import net.corda.node.services.statemachine.interceptors.HospitalisingInterceptor import net.corda.node.services.statemachine.interceptors.PrintingInterceptor import net.corda.node.utilities.AffinityExecutor -import net.corda.node.utilities.errorAndTerminate import net.corda.node.utilities.injectOldProgressTracker import net.corda.node.utilities.isEnabledTimedFlow import net.corda.nodeapi.internal.persistence.CordaPersistence @@ -83,7 +82,7 @@ internal class SingleThreadedStateMachineManager( private val scheduledFutureExecutor = Executors.newSingleThreadScheduledExecutor( ThreadFactoryBuilder().setNameFormat("flow-scheduled-future-thread").setDaemon(true).build() ) - // How many Fibers are running and not suspended. If zero and stopping is true, then we are halted. + // How many Fibers are running (this includes suspended flows). If zero and stopping is true, then we are halted. private val liveFibers = ReusableLatch() // Monitoring support. private val metrics = serviceHub.monitoringService.metrics @@ -146,13 +145,8 @@ internal class SingleThreadedStateMachineManager( val fibers = restoreFlowsFromCheckpoints() metrics.register("Flows.InFlight", Gauge { innerState.flows.size }) - Fiber.setDefaultUncaughtExceptionHandler { fiber, throwable -> - if (throwable is VirtualMachineError) { - errorAndTerminate("Caught unrecoverable error from flow. Forcibly terminating the JVM, this might leave resources open, and most likely will.", throwable) - } else { - (fiber as FlowStateMachineImpl<*>).logger.warn("Caught exception from flow", throwable) - } - } + + setFlowDefaultUncaughtExceptionHandler() val pausedFlows = restoreNonResidentFlowsFromPausedCheckpoints() innerState.withLock { @@ -175,6 +169,17 @@ internal class SingleThreadedStateMachineManager( } } + private fun setFlowDefaultUncaughtExceptionHandler() { + Fiber.setDefaultUncaughtExceptionHandler( + FlowDefaultUncaughtExceptionHandler( + flowHospital, + checkpointStorage, + database, + scheduledFutureExecutor + ) + ) + } + override fun snapshot(): Set> = innerState.flows.values.map { it.fiber }.toSet() override fun > findStateMachines(flowClass: Class): List>> { @@ -437,7 +442,7 @@ internal class SingleThreadedStateMachineManager( ?.map { it.exception } ?.plus(e) ?: emptyList() logger.info("Failed to retry flow $flowId, keeping in for observation and aborting") - flowHospital.forceIntoOvernightObservation(flowId, exceptions) + flowHospital.forceIntoOvernightObservation(currentState, exceptions) throw e } } diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/StaffedFlowHospital.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/StaffedFlowHospital.kt index 1f60bc0f2a..fc93d3afca 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/StaffedFlowHospital.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/StaffedFlowHospital.kt @@ -167,27 +167,27 @@ class StaffedFlowHospital(private val flowMessaging: FlowMessaging, } /** - * Forces the flow to be kept in for overnight observation by the hospital. A flow must already exist inside the hospital - * and have existing medical records for it to be moved to overnight observation. If it does not meet these criteria then - * an [IllegalArgumentException] will be thrown. + * Forces the flow to be kept in for overnight observation by the hospital. * - * @param id The [StateMachineRunId] of the flow that you are trying to force into observation + * @param currentState The [StateMachineState] of the flow that is being forced into observation * @param errors The errors to include in the new medical record */ - fun forceIntoOvernightObservation(id: StateMachineRunId, errors: List) { + fun forceIntoOvernightObservation(currentState: StateMachineState, errors: List) { mutex.locked { - // If a flow does not meet the criteria below, then it has moved into an invalid state or the function is being - // called from an incorrect location. The assertions below should error out the flow if they are not true. - requireNotNull(flowsInHospital[id]) { "Flow must already be in the hospital before forcing into overnight observation" } - val history = requireNotNull(flowPatients[id]) { "Flow must already have history before forcing into overnight observation" } - // Use the last staff member that last discharged the flow as the current staff member - val record = history.records.last().copy( + val id = currentState.flowLogic.runId + val medicalHistory = flowPatients.computeIfAbsent(id) { FlowMedicalHistory() } + val record = MedicalRecord.Flow( time = clock.instant(), + flowId = id, + suspendCount = currentState.checkpoint.checkpointState.numberOfSuspends, errors = errors, + by = listOf(TransitionErrorGeneralPractitioner), outcome = Outcome.OVERNIGHT_OBSERVATION ) + + medicalHistory.records += record + onFlowKeptForOvernightObservation.forEach { hook -> hook.invoke(id, record.by.map { it.toString() }) } - history.records += record recordsPublisher.onNext(record) } } diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/DBCheckpointStorageTests.kt b/node/src/test/kotlin/net/corda/node/services/persistence/DBCheckpointStorageTests.kt index a75960523b..3c526e4a9d 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/DBCheckpointStorageTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/DBCheckpointStorageTests.kt @@ -872,6 +872,24 @@ class DBCheckpointStorageTests { } } + @Test(timeout = 300_000) + fun `update only the flow status`() { + val (id, checkpoint) = newCheckpoint() + val serializedFlowState = checkpoint.serializeFlowState() + database.transaction { + checkpointStorage.addCheckpoint(id, checkpoint, serializedFlowState, checkpoint.serializeCheckpointState()) + } + database.transaction { + checkpointStorage.updateStatus(id, Checkpoint.FlowStatus.HOSPITALIZED) + } + database.transaction { + assertEquals( + checkpoint.copy(status = Checkpoint.FlowStatus.HOSPITALIZED), + checkpointStorage.checkpoints().single().deserialize() + ) + } + } + data class IdAndCheckpoint(val id: StateMachineRunId, val checkpoint: Checkpoint) private fun changeStatus(oldCheckpoint: Checkpoint, status: Checkpoint.FlowStatus): IdAndCheckpoint { From 9a8ae0fd326cbaf4ae49532a3f95f10c37c4257e Mon Sep 17 00:00:00 2001 From: Dan Newton Date: Tue, 7 Jul 2020 08:47:09 +0100 Subject: [PATCH 53/85] CORDA-3841 Update session init flow error handling tests (#6431) These tests were removed after doing a merge from 4.4. They needed updating after the changes from 4.4 anyway. These have been included in this change. Also fix kill flow tests and send initial tests. --- .../StateMachineFlowInitErrorHandlingTest.kt | 134 ++++++++++++++++ .../StateMachineGeneralErrorHandlingTest.kt | 147 +++++++++++++++++- .../StateMachineKillFlowErrorHandlingTest.kt | 33 ++-- 3 files changed, 284 insertions(+), 30 deletions(-) diff --git a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineFlowInitErrorHandlingTest.kt b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineFlowInitErrorHandlingTest.kt index 8e925ec6a1..9f3e0a5b66 100644 --- a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineFlowInitErrorHandlingTest.kt +++ b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineFlowInitErrorHandlingTest.kt @@ -4,6 +4,7 @@ import net.corda.core.CordaRuntimeException import net.corda.core.messaging.startFlow import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.seconds +import net.corda.node.services.api.CheckpointStorage import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.CHARLIE_NAME import net.corda.testing.core.singleIdentity @@ -12,6 +13,7 @@ import org.junit.Test import java.sql.Connection import java.util.concurrent.ExecutorService import java.util.concurrent.Executors +import java.util.concurrent.TimeoutException import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertTrue @@ -444,4 +446,136 @@ class StateMachineFlowInitErrorHandlingTest : StateMachineErrorHandlingTest() { charlie2.rpc.assertNumberOfCheckpoints(0) } } + + /** + * Throws an exception when performing an [Action.CommitTransaction] event before the flow has suspended (remains in an unstarted + * state) on a responding node. + * + * The exception is thrown 3 times. + * + * An exception is also thrown from [CheckpointStorage.getCheckpoint]. + * + * This test is to prevent a regression, where a transient database connection error can be thrown retrieving a flow's checkpoint when + * retrying the flow after it failed to commit it's original checkpoint. + * + * This causes the transition to be discharged from the hospital 3 times (retries 3 times). On the final retry the transition + * succeeds and the flow finishes. + */ + @Test(timeout = 300_000) + fun `responding flow - session init can be retried when there is a transient connection error to the database`() { + startDriver { + val (charlie, port) = createBytemanNode(CHARLIE_NAME) + val alice = createNode(ALICE_NAME) + + val rules = """ + RULE Create Counter + CLASS ${ActionExecutorImpl::class.java.name} + METHOD executeCommitTransaction + AT ENTRY + IF createCounter("counter", $counter) + DO traceln("Counter created") + ENDRULE + + RULE Throw exception on executeCommitTransaction action + CLASS ${ActionExecutorImpl::class.java.name} + METHOD executeCommitTransaction + AT ENTRY + IF readCounter("counter") < 3 + DO incrementCounter("counter"); traceln("Throwing exception"); throw new java.lang.RuntimeException("die dammit die") + ENDRULE + + RULE Throw exception on getCheckpoint + INTERFACE ${CheckpointStorage::class.java.name} + METHOD getCheckpoint + AT ENTRY + IF true + DO traceln("Throwing exception getting checkpoint"); throw new java.sql.SQLTransientConnectionException("Connection is not available") + ENDRULE + """.trimIndent() + + submitBytemanRules(rules, port) + + alice.rpc.startFlow( + StateMachineErrorHandlingTest::SendAMessageFlow, + charlie.nodeInfo.singleIdentity() + ).returnValue.getOrThrow( + 30.seconds + ) + + charlie.rpc.assertHospitalCounts( + discharged = 3, + observation = 0 + ) + assertEquals(0, alice.rpc.stateMachinesSnapshot().size) + assertEquals(0, charlie.rpc.stateMachinesSnapshot().size) + assertEquals(0, charlie.rpc.startFlow(StateMachineErrorHandlingTest::GetNumberOfCheckpointsFlow).returnValue.get()) + } + } + + /** + * Throws an exception when performing an [Action.CommitTransaction] event before the flow has suspended (remains in an unstarted + * state) on a responding node. + * + * The exception is thrown 4 times. + * + * An exception is also thrown from [CheckpointStorage.getCheckpoint]. + * + * This test is to prevent a regression, where a transient database connection error can be thrown retrieving a flow's checkpoint when + * retrying the flow after it failed to commit it's original checkpoint. + * + * This causes the transition to be discharged from the hospital 3 times (retries 3 times). On the final retry the transition + * fails and is kept for in for observation. + */ + @Test(timeout = 300_000) + fun `responding flow - session init can be retried when there is a transient connection error to the database goes to observation if error persists`() { + startDriver { + val (charlie, port) = createBytemanNode(CHARLIE_NAME) + val alice = createNode(ALICE_NAME) + + val rules = """ + RULE Create Counter + CLASS ${ActionExecutorImpl::class.java.name} + METHOD executeCommitTransaction + AT ENTRY + IF createCounter("counter", $counter) + DO traceln("Counter created") + ENDRULE + + RULE Throw exception on executeCommitTransaction action + CLASS ${ActionExecutorImpl::class.java.name} + METHOD executeCommitTransaction + AT ENTRY + IF readCounter("counter") < 4 + DO incrementCounter("counter"); traceln("Throwing exception"); throw new java.lang.RuntimeException("die dammit die") + ENDRULE + + RULE Throw exception on getCheckpoint + INTERFACE ${CheckpointStorage::class.java.name} + METHOD getCheckpoint + AT ENTRY + IF true + DO traceln("Throwing exception getting checkpoint"); throw new java.sql.SQLTransientConnectionException("Connection is not available") + ENDRULE + """.trimIndent() + + submitBytemanRules(rules, port) + + assertFailsWith { + alice.rpc.startFlow( + StateMachineErrorHandlingTest::SendAMessageFlow, + charlie.nodeInfo.singleIdentity() + ).returnValue.getOrThrow( + 30.seconds + ) + } + + charlie.rpc.assertHospitalCounts( + discharged = 3, + observation = 1 + ) + assertEquals(1, alice.rpc.stateMachinesSnapshot().size) + assertEquals(1, charlie.rpc.stateMachinesSnapshot().size) + assertEquals(0, charlie.rpc.startFlow(StateMachineErrorHandlingTest::GetNumberOfCheckpointsFlow).returnValue.get()) + } + } } \ No newline at end of file diff --git a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineGeneralErrorHandlingTest.kt b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineGeneralErrorHandlingTest.kt index 62c18d4107..8e3f44597a 100644 --- a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineGeneralErrorHandlingTest.kt +++ b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineGeneralErrorHandlingTest.kt @@ -4,12 +4,15 @@ import net.corda.core.CordaRuntimeException import net.corda.core.messaging.startFlow import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.seconds +import net.corda.node.services.api.CheckpointStorage import net.corda.node.services.messaging.DeduplicationHandler import net.corda.node.services.statemachine.transitions.TopLevelTransition import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.CHARLIE_NAME import net.corda.testing.core.singleIdentity import org.junit.Test +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors import java.util.concurrent.TimeoutException import kotlin.test.assertEquals import kotlin.test.assertFailsWith @@ -17,6 +20,10 @@ import kotlin.test.assertFailsWith @Suppress("MaxLineLength") // Byteman rules cannot be easily wrapped class StateMachineGeneralErrorHandlingTest : StateMachineErrorHandlingTest() { + private companion object { + val executor: ExecutorService = Executors.newSingleThreadExecutor() + } + /** * Throws an exception when performing an [Action.SendInitial] action. * @@ -34,15 +41,15 @@ class StateMachineGeneralErrorHandlingTest : StateMachineErrorHandlingTest() { val rules = """ RULE Create Counter CLASS ${ActionExecutorImpl::class.java.name} - METHOD executeSendInitial + METHOD executeSendMultiple AT ENTRY IF createCounter("counter", $counter) DO traceln("Counter created") ENDRULE - RULE Throw exception on executeSendInitial action + RULE Throw exception on executeSendMultiple action CLASS ${ActionExecutorImpl::class.java.name} - METHOD executeSendInitial + METHOD executeSendMultiple AT ENTRY IF readCounter("counter") < 4 DO incrementCounter("counter"); traceln("Throwing exception"); throw new java.lang.RuntimeException("die dammit die") @@ -86,15 +93,15 @@ class StateMachineGeneralErrorHandlingTest : StateMachineErrorHandlingTest() { val rules = """ RULE Create Counter CLASS ${ActionExecutorImpl::class.java.name} - METHOD executeSendInitial + METHOD executeSendMultiple AT ENTRY IF createCounter("counter", $counter) DO traceln("Counter created") ENDRULE - RULE Throw exception on executeSendInitial action + RULE Throw exception on executeSendMultiple action CLASS ${ActionExecutorImpl::class.java.name} - METHOD executeSendInitial + METHOD executeSendMultiple AT ENTRY IF readCounter("counter") < 3 DO incrementCounter("counter"); traceln("Throwing exception"); throw new java.lang.RuntimeException("die dammit die") @@ -464,6 +471,134 @@ class StateMachineGeneralErrorHandlingTest : StateMachineErrorHandlingTest() { } } + /** + * Throws an exception when performing an [Action.CommitTransaction] event before the flow has suspended (remains in an unstarted + * state). + * + * The exception is thrown 3 times. + * + * An exception is also thrown from [CheckpointStorage.getCheckpoint]. + * + * This test is to prevent a regression, where a transient database connection error can be thrown retrieving a flow's checkpoint when + * retrying the flow after it failed to commit it's original checkpoint. + * + * This causes the transition to be discharged from the hospital 3 times (retries 3 times). On the final retry the transition + * succeeds and the flow finishes. + */ + @Test(timeout = 300_000) + fun `flow can be retried when there is a transient connection error to the database`() { + startDriver { + val charlie = createNode(CHARLIE_NAME) + val (alice, port) = createBytemanNode(ALICE_NAME) + + val rules = """ + RULE Create Counter + CLASS ${ActionExecutorImpl::class.java.name} + METHOD executeCommitTransaction + AT ENTRY + IF createCounter("counter", $counter) + DO traceln("Counter created") + ENDRULE + + RULE Throw exception on executeCommitTransaction action + CLASS ${ActionExecutorImpl::class.java.name} + METHOD executeCommitTransaction + AT ENTRY + IF readCounter("counter") < 3 + DO incrementCounter("counter"); traceln("Throwing exception"); throw new java.lang.RuntimeException("die dammit die") + ENDRULE + + RULE Throw exception on getCheckpoint + INTERFACE ${CheckpointStorage::class.java.name} + METHOD getCheckpoint + AT ENTRY + IF true + DO traceln("Throwing exception getting checkpoint"); throw new java.sql.SQLTransientConnectionException("Connection is not available") + ENDRULE + """.trimIndent() + + submitBytemanRules(rules, port) + + alice.rpc.startFlow( + StateMachineErrorHandlingTest::SendAMessageFlow, + charlie.nodeInfo.singleIdentity() + ).returnValue.getOrThrow( + 30.seconds + ) + + alice.rpc.assertHospitalCounts( + discharged = 3, + observation = 0 + ) + assertEquals(0, alice.rpc.stateMachinesSnapshot().size) + assertEquals(0, alice.rpc.startFlow(StateMachineErrorHandlingTest::GetNumberOfCheckpointsFlow).returnValue.get()) + } + } + + /** + * Throws an exception when performing an [Action.CommitTransaction] event before the flow has suspended (remains in an unstarted + * state). + * + * The exception is thrown 4 times. + * + * An exception is also thrown from [CheckpointStorage.getCheckpoint]. + * + * This test is to prevent a regression, where a transient database connection error can be thrown retrieving a flow's checkpoint when + * retrying the flow after it failed to commit it's original checkpoint. + * + * This causes the transition to be discharged from the hospital 3 times (retries 3 times). On the final retry the transition + * fails and is kept for in for observation. + */ + @Test(timeout = 300_000) + fun `flow can be retried when there is a transient connection error to the database goes to observation if error persists`() { + startDriver { + val charlie = createNode(CHARLIE_NAME) + val (alice, port) = createBytemanNode(ALICE_NAME) + + val rules = """ + RULE Create Counter + CLASS ${ActionExecutorImpl::class.java.name} + METHOD executeCommitTransaction + AT ENTRY + IF createCounter("counter", $counter) + DO traceln("Counter created") + ENDRULE + + RULE Throw exception on executeCommitTransaction action + CLASS ${ActionExecutorImpl::class.java.name} + METHOD executeCommitTransaction + AT ENTRY + IF readCounter("counter") < 4 + DO incrementCounter("counter"); traceln("Throwing exception"); throw new java.lang.RuntimeException("die dammit die") + ENDRULE + + RULE Throw exception on getCheckpoint + INTERFACE ${CheckpointStorage::class.java.name} + METHOD getCheckpoint + AT ENTRY + IF true + DO traceln("Throwing exception getting checkpoint"); throw new java.sql.SQLTransientConnectionException("Connection is not available") + ENDRULE + """.trimIndent() + + submitBytemanRules(rules, port) + + executor.execute { + alice.rpc.startFlow(StateMachineErrorHandlingTest::SendAMessageFlow, charlie.nodeInfo.singleIdentity()) + } + + // flow is not signaled as started calls to [getOrThrow] will hang, sleeping instead + Thread.sleep(30.seconds.toMillis()) + + alice.rpc.assertHospitalCounts( + discharged = 3, + observation = 1 + ) + assertEquals(1, alice.rpc.stateMachinesSnapshot().size) + assertEquals(0, alice.rpc.startFlow(StateMachineErrorHandlingTest::GetNumberOfCheckpointsFlow).returnValue.get()) + } + } + /** * Throws an exception when performing an [Action.CommitTransaction] event when the flow is finishing on a responding node. * diff --git a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineKillFlowErrorHandlingTest.kt b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineKillFlowErrorHandlingTest.kt index ae07eb60d4..31f73c526d 100644 --- a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineKillFlowErrorHandlingTest.kt +++ b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineKillFlowErrorHandlingTest.kt @@ -2,6 +2,7 @@ package net.corda.node.services.statemachine import co.paralleluniverse.fibers.Suspendable import net.corda.core.flows.FlowLogic +import net.corda.core.flows.KilledFlowException import net.corda.core.flows.StartableByRPC import net.corda.core.messaging.startFlow import net.corda.core.messaging.startTrackedFlow @@ -30,21 +31,9 @@ class StateMachineKillFlowErrorHandlingTest : StateMachineErrorHandlingTest() { * No pass through the hospital is recorded. As the flow is marked as `isRemoved`. */ @Test(timeout = 300_000) - fun `error during transition due to an InterruptedException (killFlow) will terminate the flow`() { + fun `error during transition due to killing a flow will terminate the flow`() { startDriver { - val (alice, port) = createBytemanNode(ALICE_NAME) - - val rules = """ - RULE Increment terminal counter - CLASS ${StaffedFlowHospital.TransitionErrorGeneralPractitioner::class.java.name} - METHOD consult - AT READ TERMINAL - IF true - DO traceln("Byteman test - terminal") - ENDRULE - """.trimIndent() - - submitBytemanRules(rules, port) + val alice = createNode(ALICE_NAME) val flow = alice.rpc.startTrackedFlow(StateMachineKillFlowErrorHandlingTest::SleepFlow) @@ -56,14 +45,10 @@ class StateMachineKillFlowErrorHandlingTest : StateMachineErrorHandlingTest() { } } - assertFailsWith { flow.returnValue.getOrThrow(20.seconds) } - - val output = getBytemanOutput(alice) + assertFailsWith { flow.returnValue.getOrThrow(20.seconds) } assertTrue(flowKilled) - val numberOfTerminalDiagnoses = output.filter { it.contains("Byteman test - terminal") }.size - assertEquals(1, numberOfTerminalDiagnoses) - alice.rpc.assertHospitalCounts(propagated = 1) + alice.rpc.assertHospitalCountsAllZero() assertEquals(0, alice.rpc.stateMachinesSnapshot().size) alice.rpc.assertNumberOfCheckpoints(0) } @@ -97,7 +82,7 @@ class StateMachineKillFlowErrorHandlingTest : StateMachineErrorHandlingTest() { } } - assertFailsWith { flow.returnValue.getOrThrow(30.seconds) } + assertFailsWith { flow.returnValue.getOrThrow(30.seconds) } assertTrue(flowKilled) alice.rpc.assertHospitalCountsAllZero() @@ -124,15 +109,15 @@ class StateMachineKillFlowErrorHandlingTest : StateMachineErrorHandlingTest() { val rules = """ RULE Create Counter CLASS ${ActionExecutorImpl::class.java.name} - METHOD executeSendInitial + METHOD executeSendMultiple AT ENTRY IF createCounter("counter", $counter) DO traceln("Counter created") ENDRULE - RULE Throw exception on executeSendInitial action + RULE Throw exception on executeSendMultiple action CLASS ${ActionExecutorImpl::class.java.name} - METHOD executeSendInitial + METHOD executeSendMultiple AT ENTRY IF readCounter("counter") < 4 DO incrementCounter("counter"); traceln("Throwing exception"); throw new java.lang.RuntimeException("die dammit die") From b619356bffe81360ba70944c1c3fc7bd2d0f8c99 Mon Sep 17 00:00:00 2001 From: Kyriakos Tharrouniatis Date: Tue, 7 Jul 2020 09:07:01 +0100 Subject: [PATCH 54/85] Reduce exception_message and stack_trace lengths in table node_flow_exceptions (#6432) Reduce exception_message and stack_trace lengths in table node_flow_exceptions from 4000 to 2000 to fix Oracle failing with: 'ORA-00910: specified length too long for its datatype' --- .../corda/node/services/persistence/DBCheckpointStorage.kt | 4 ++-- .../resources/migration/node-core.changelog-v19-postgres.xml | 4 ++-- node/src/main/resources/migration/node-core.changelog-v19.xml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBCheckpointStorage.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBCheckpointStorage.kt index a9072afdab..38cbf1d833 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBCheckpointStorage.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBCheckpointStorage.kt @@ -50,8 +50,8 @@ class DBCheckpointStorage( private const val HMAC_SIZE_BYTES = 16 @VisibleForTesting - const val MAX_STACKTRACE_LENGTH = 4000 - private const val MAX_EXC_MSG_LENGTH = 4000 + const val MAX_STACKTRACE_LENGTH = 2000 + private const val MAX_EXC_MSG_LENGTH = 2000 private const val MAX_EXC_TYPE_LENGTH = 256 private const val MAX_FLOW_NAME_LENGTH = 128 private const val MAX_PROGRESS_STEP_LENGTH = 256 diff --git a/node/src/main/resources/migration/node-core.changelog-v19-postgres.xml b/node/src/main/resources/migration/node-core.changelog-v19-postgres.xml index 3c8e4d92ad..6aedc510b4 100644 --- a/node/src/main/resources/migration/node-core.changelog-v19-postgres.xml +++ b/node/src/main/resources/migration/node-core.changelog-v19-postgres.xml @@ -72,10 +72,10 @@ - + - + diff --git a/node/src/main/resources/migration/node-core.changelog-v19.xml b/node/src/main/resources/migration/node-core.changelog-v19.xml index 761bdef715..6b8c1e9b24 100644 --- a/node/src/main/resources/migration/node-core.changelog-v19.xml +++ b/node/src/main/resources/migration/node-core.changelog-v19.xml @@ -72,10 +72,10 @@ - + - + From 8175d4f0fb89b9d9942d557a0dbc3f52656475a4 Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Tue, 7 Jul 2020 09:07:55 +0100 Subject: [PATCH 55/85] CORDA-3892: Upgrade Gradle 5.4.1 -> 5.6.4. (#6435) --- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 5 ++--- gradlew | 6 +++--- gradlew.bat | 2 +- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/build.gradle b/build.gradle index 6a3c888a5b..872fb36280 100644 --- a/build.gradle +++ b/build.gradle @@ -662,7 +662,7 @@ if (file('corda-docs-only-build').exists() || (System.getenv('CORDA_DOCS_ONLY_BU } wrapper { - gradleVersion = "5.4.1" + gradleVersion = "5.6.4" distributionType = Wrapper.DistributionType.ALL } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2e954d6bc7..29c1f86072 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Wed Aug 21 10:48:19 BST 2019 -distributionUrl=https\://gradleproxy:gradleproxy@software.r3.com/artifactory/gradle-proxy/gradle-5.4.1-all.zip distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -zipStorePath=wrapper/dists +distributionUrl=https\://gradleproxy:gradleproxy@software.r3.com/artifactory/gradle-proxy/gradle-5.6.4-all.zip zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index b0d6d0ab5d..83f2acfdc3 100755 --- a/gradlew +++ b/gradlew @@ -7,7 +7,7 @@ # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, @@ -125,8 +125,8 @@ if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` diff --git a/gradlew.bat b/gradlew.bat index 15e1ee37a7..24467a141f 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -5,7 +5,7 @@ @rem you may not use this file except in compliance with the License. @rem You may obtain a copy of the License at @rem -@rem http://www.apache.org/licenses/LICENSE-2.0 +@rem https://www.apache.org/licenses/LICENSE-2.0 @rem @rem Unless required by applicable law or agreed to in writing, software @rem distributed under the License is distributed on an "AS IS" BASIS, From 16eb9dfc08c8cadbd673e4b88f9e2a446dc2b4e3 Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Tue, 7 Jul 2020 09:08:58 +0100 Subject: [PATCH 56/85] NOTICK: Fix some Gradle technical debt. (#6430) --- build.gradle | 14 +++++++------- core-tests/build.gradle | 2 +- deterministic.gradle | 4 ++-- experimental/avalanche/build.gradle | 5 ++++- java8.gradle | 4 ++-- node/build.gradle | 4 ++-- node/capsule/build.gradle | 3 +-- samples/irs-demo/cordapp/build.gradle | 2 +- serialization-djvm/build.gradle | 2 +- testing/testserver/testcapsule/build.gradle | 3 +-- tools/demobench/build.gradle | 2 +- tools/error-tool/build.gradle | 10 ++++------ tools/explorer/build.gradle | 4 ++-- tools/explorer/capsule/build.gradle | 3 +-- tools/network-builder/build.gradle | 12 ++++++------ tools/shell-cli/build.gradle | 11 ++++++----- tools/shell/build.gradle | 2 +- 17 files changed, 43 insertions(+), 44 deletions(-) diff --git a/build.gradle b/build.gradle index 872fb36280..bf4959cb80 100644 --- a/build.gradle +++ b/build.gradle @@ -275,7 +275,7 @@ allprojects { toolVersion = "0.8.3" } - tasks.withType(JavaCompile) { + tasks.withType(JavaCompile).configureEach { options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation" << "-Xlint:-options" << "-parameters" options.compilerArgs << '-XDenableSunApiLintControl' if (warnings_as_errors) { @@ -287,7 +287,7 @@ allprojects { options.encoding = 'UTF-8' } - tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile) { + tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { kotlinOptions { languageVersion = "1.2" apiVersion = "1.2" @@ -302,7 +302,7 @@ allprojects { task.dependsOn tasks.withType(AbstractCompile) } - tasks.withType(Jar) { task -> + tasks.withType(Jar).configureEach { task -> // Includes War and Ear manifest { attributes('Corda-Release-Version': corda_release_version) @@ -314,7 +314,7 @@ allprojects { } } - tasks.withType(Test) { + tasks.withType(Test).configureEach { forkEvery = 10 ignoreFailures = project.hasProperty('tests.ignoreFailures') ? project.property('tests.ignoreFailures').toBoolean() : false failFast = project.hasProperty('tests.failFast') ? project.property('tests.failFast').toBoolean() : false @@ -339,7 +339,7 @@ allprojects { systemProperty 'java.security.egd', 'file:/dev/./urandom' } - tasks.withType(Test) { + tasks.withType(Test).configureEach { if (name.contains("integrationTest")) { maxParallelForks = (System.env.CORDA_INT_TESTING_FORKS == null) ? 1 : "$System.env.CORDA_INT_TESTING_FORKS".toInteger() } @@ -520,7 +520,7 @@ tasks.register('detektBaseline', JavaExec) { args(params) } -tasks.withType(Test) { +tasks.withType(Test).configureEach { reports.html.destination = file("${reporting.baseDir}/${name}") } @@ -626,7 +626,7 @@ dependxiesModule { skipTasks = "test,integrationTest,smokeTest,slowIntegrationTest" } -task generateApi(type: net.corda.plugins.GenerateApi) { +tasks.register('generateApi', net.corda.plugins.GenerateApi) { baseName = "api-corda" } diff --git a/core-tests/build.gradle b/core-tests/build.gradle index 8d1919f474..eb21b557f5 100644 --- a/core-tests/build.gradle +++ b/core-tests/build.gradle @@ -99,7 +99,7 @@ configurations { testArtifacts.extendsFrom testRuntimeClasspath } -tasks.withType(Test) { +tasks.withType(Test).configureEach { // fork a new test process for every test class forkEvery = 10 } diff --git a/deterministic.gradle b/deterministic.gradle index 2a7913d426..11bd05770f 100644 --- a/deterministic.gradle +++ b/deterministic.gradle @@ -16,7 +16,7 @@ ext { deterministic_rt_jar = jdk8uDeterministic.rt_jar } -tasks.withType(AbstractCompile) { +tasks.withType(AbstractCompile).configureEach { dependsOn jdkTask // This is a bit ugly, but Gradle isn't recognising the KotlinCompile task @@ -29,7 +29,7 @@ tasks.withType(AbstractCompile) { } } -tasks.withType(JavaCompile) { +tasks.withType(JavaCompile).configureEach { options.compilerArgs << '-bootclasspath' << deterministic_rt_jar sourceCompatibility = VERSION_1_8 targetCompatibility = VERSION_1_8 diff --git a/experimental/avalanche/build.gradle b/experimental/avalanche/build.gradle index e6f9c1ccae..540bc2947b 100644 --- a/experimental/avalanche/build.gradle +++ b/experimental/avalanche/build.gradle @@ -22,4 +22,7 @@ jar.enabled = false shadowJar { baseName = "avalanche" } -assemble.dependsOn shadowJar + +artifacts { + archives shadowJar +} diff --git a/java8.gradle b/java8.gradle index e0fbf629cc..50a462aa41 100644 --- a/java8.gradle +++ b/java8.gradle @@ -6,7 +6,7 @@ import static org.gradle.api.JavaVersion.VERSION_1_8 */ apply plugin: 'kotlin' -tasks.withType(AbstractCompile) { +tasks.withType(AbstractCompile).configureEach { // This is a bit ugly, but Gradle isn't recognising the KotlinCompile task // as it does the built-in JavaCompile task. if (it.class.name.startsWith('org.jetbrains.kotlin.gradle.tasks.KotlinCompile')) { @@ -16,7 +16,7 @@ tasks.withType(AbstractCompile) { } } -tasks.withType(JavaCompile) { +tasks.withType(JavaCompile).configureEach { sourceCompatibility = VERSION_1_8 targetCompatibility = VERSION_1_8 } diff --git a/node/build.gradle b/node/build.gradle index 60e071bbb6..db3cc95796 100644 --- a/node/build.gradle +++ b/node/build.gradle @@ -243,12 +243,12 @@ dependencies { testCompile project(':testing:cordapps:dbfailure:dbfworkflows') } -tasks.withType(JavaCompile) { +tasks.withType(JavaCompile).configureEach { // Resolves a Gradle warning about not scanning for pre-processors. options.compilerArgs << '-proc:none' } -tasks.withType(Test) { +tasks.withType(Test).configureEach { if (JavaVersion.current() == JavaVersion.VERSION_11) { jvmArgs '-Djdk.attach.allowAttachSelf=true' } diff --git a/node/capsule/build.gradle b/node/capsule/build.gradle index 78b1a48970..14cddd0332 100644 --- a/node/capsule/build.gradle +++ b/node/capsule/build.gradle @@ -119,9 +119,8 @@ task buildCordaJAR(type: FatCapsule, dependsOn: [ } } -assemble.dependsOn buildCordaJAR - artifacts { + archives buildCordaJAR runtimeArtifacts buildCordaJAR publish buildCordaJAR { classifier '' diff --git a/samples/irs-demo/cordapp/build.gradle b/samples/irs-demo/cordapp/build.gradle index bf6c020cdf..50474dd3e5 100644 --- a/samples/irs-demo/cordapp/build.gradle +++ b/samples/irs-demo/cordapp/build.gradle @@ -153,7 +153,7 @@ task integrationTest(type: Test, dependsOn: []) { // This fixes the "line too long" error when running this demo with windows CLI // TODO: Automatically apply to all projects via a plugin -tasks.withType(CreateStartScripts).each { task -> +tasks.withType(CreateStartScripts).configureEach { task -> task.doLast { String text = task.windowsScript.text // Replaces the per file classpath (which are all jars in "lib") with a wildcard on lib diff --git a/serialization-djvm/build.gradle b/serialization-djvm/build.gradle index 7ec41f9d32..f51557e2a3 100644 --- a/serialization-djvm/build.gradle +++ b/serialization-djvm/build.gradle @@ -56,7 +56,7 @@ jar { } } -tasks.withType(Test) { +tasks.withType(Test).configureEach { useJUnitPlatform() systemProperty 'deterministic-rt.path', configurations.jdkRt.asPath systemProperty 'sandbox-libraries.path', configurations.sandboxTesting.asPath diff --git a/testing/testserver/testcapsule/build.gradle b/testing/testserver/testcapsule/build.gradle index f69d05b263..f231f12c68 100644 --- a/testing/testserver/testcapsule/build.gradle +++ b/testing/testserver/testcapsule/build.gradle @@ -64,9 +64,8 @@ task buildWebserverJar(type: FatCapsule, dependsOn: project(':node').tasks.jar) } } -assemble.dependsOn buildWebserverJar - artifacts { + archives buildWebserverJar runtimeArtifacts buildWebserverJar publish buildWebserverJar { classifier '' diff --git a/tools/demobench/build.gradle b/tools/demobench/build.gradle index 1522d4b82d..be75806d58 100644 --- a/tools/demobench/build.gradle +++ b/tools/demobench/build.gradle @@ -90,7 +90,7 @@ dependencies { testCompile "junit:junit:$junit_version" } -tasks.withType(JavaCompile) { +tasks.withType(JavaCompile).configureEach { // Resolves a Gradle warning about not scanning for pre-processors. options.compilerArgs << '-proc:none' } diff --git a/tools/error-tool/build.gradle b/tools/error-tool/build.gradle index d1e11ec376..908775f7c2 100644 --- a/tools/error-tool/build.gradle +++ b/tools/error-tool/build.gradle @@ -1,17 +1,13 @@ apply plugin: 'kotlin' apply plugin: 'com.github.johnrengelman.shadow' -repositories { - mavenCentral() -} - dependencies { implementation project(":common-logging") implementation project(":tools:cliutils") implementation "info.picocli:picocli:$picocli_version" implementation "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version" - testCompile "junit:junit:4.12" + testImplementation "junit:junit:$junit_version" } jar { @@ -28,4 +24,6 @@ shadowJar { } } -assemble.dependsOn shadowJar \ No newline at end of file +artifacts { + archives shadowJar +} diff --git a/tools/explorer/build.gradle b/tools/explorer/build.gradle index f4bc37cee9..82838e5c80 100644 --- a/tools/explorer/build.gradle +++ b/tools/explorer/build.gradle @@ -71,7 +71,7 @@ dependencies { compile "net.sf.jopt-simple:jopt-simple:$jopt_simple_version" } -tasks.withType(JavaCompile) { +tasks.withType(JavaCompile).configureEach { // Resolves a Gradle warning about not scanning for pre-processors. options.compilerArgs << '-proc:none' } @@ -82,4 +82,4 @@ jar { 'Automatic-Module-Name': 'net.corda.tools.explorer' ) } -} \ No newline at end of file +} diff --git a/tools/explorer/capsule/build.gradle b/tools/explorer/capsule/build.gradle index 05750ee7d6..56b5f3a5de 100644 --- a/tools/explorer/capsule/build.gradle +++ b/tools/explorer/capsule/build.gradle @@ -41,9 +41,8 @@ task buildExplorerJAR(type: FatCapsule, dependsOn: project(':tools:explorer').ta } } -assemble.dependsOn buildExplorerJAR - artifacts { + archives buildExplorerJAR runtimeArtifacts buildExplorerJAR publish buildExplorerJAR { classifier "" diff --git a/tools/network-builder/build.gradle b/tools/network-builder/build.gradle index 3306ac7c15..01c65cdcd4 100644 --- a/tools/network-builder/build.gradle +++ b/tools/network-builder/build.gradle @@ -62,7 +62,7 @@ dependencies { compile "org.controlsfx:controlsfx:$controlsfx_version" } -tasks.withType(JavaCompile) { +tasks.withType(JavaCompile).configureEach { // Resolves a Gradle warning about not scanning for pre-processors. options.compilerArgs << '-proc:none' } @@ -78,13 +78,13 @@ shadowJar { zip64 true } -task buildNetworkBuilder(dependsOn: shadowJar) -assemble.dependsOn buildNetworkBuilder +tasks.register('buildNetworkBuilder') { + dependsOn shadowJar +} artifacts { - publish shadowJar { - archiveClassifier = jdkClassifier - } + archives shadowJar + publish shadowJar } jar { diff --git a/tools/shell-cli/build.gradle b/tools/shell-cli/build.gradle index 31214428cb..80ebd521dd 100644 --- a/tools/shell-cli/build.gradle +++ b/tools/shell-cli/build.gradle @@ -27,16 +27,17 @@ processResources { } shadowJar { + archiveClassifier = jdkClassifier mergeServiceFiles() } -task buildShellCli(dependsOn: shadowJar) -assemble.dependsOn buildShellCli +tasks.register('buildShellCli') { + dependsOn shadowJar +} artifacts { - publish shadowJar { - archiveClassifier = jdkClassifier - } + archives shadowJar + publish shadowJar } jar { diff --git a/tools/shell/build.gradle b/tools/shell/build.gradle index f76070853b..4e6f55fda7 100644 --- a/tools/shell/build.gradle +++ b/tools/shell/build.gradle @@ -71,7 +71,7 @@ dependencies { integrationTestCompile project(':node-driver') } -tasks.withType(JavaCompile) { +tasks.withType(JavaCompile).configureEach { // Resolves a Gradle warning about not scanning for pre-processors. options.compilerArgs << '-proc:none' } From 2d63804560df1724352282a2caad02b1af0835ae Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Tue, 7 Jul 2020 11:29:21 +0100 Subject: [PATCH 57/85] CORDA-3892: Revert back to Gradle 5.4.1 to fix "clean" task. (#6439) --- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index bf4959cb80..f2641e180f 100644 --- a/build.gradle +++ b/build.gradle @@ -662,7 +662,7 @@ if (file('corda-docs-only-build').exists() || (System.getenv('CORDA_DOCS_ONLY_BU } wrapper { - gradleVersion = "5.6.4" + gradleVersion = "5.4.1" distributionType = Wrapper.DistributionType.ALL } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 29c1f86072..ea1dc6ccf6 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://gradleproxy:gradleproxy@software.r3.com/artifactory/gradle-proxy/gradle-5.6.4-all.zip +distributionUrl=https\://gradleproxy:gradleproxy@software.r3.com/artifactory/gradle-proxy/gradle-5.4.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 72938fdf3884e93e25103a739c5d8a3ee1ef7e6d Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Wed, 8 Jul 2020 11:34:25 +0100 Subject: [PATCH 58/85] CORDA-3892: Upgrade to Gradle 5.6.4 again, with build fixes. (#6448) --- build.gradle | 2 +- core-tests/build.gradle | 1 - gradle/wrapper/gradle-wrapper.properties | 2 +- node/build.gradle | 11 ++++++++--- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/build.gradle b/build.gradle index f2641e180f..5286b90831 100644 --- a/build.gradle +++ b/build.gradle @@ -662,7 +662,7 @@ if (file('corda-docs-only-build').exists() || (System.getenv('CORDA_DOCS_ONLY_BU } wrapper { - gradleVersion = "5.4.1" + gradleVersion = '5.6.4' distributionType = Wrapper.DistributionType.ALL } diff --git a/core-tests/build.gradle b/core-tests/build.gradle index eb21b557f5..c5e184e448 100644 --- a/core-tests/build.gradle +++ b/core-tests/build.gradle @@ -1,7 +1,6 @@ apply plugin: 'kotlin' apply plugin: 'kotlin-jpa' apply plugin: 'net.corda.plugins.quasar-utils' -apply plugin: 'net.corda.plugins.publish-utils' description 'Corda core tests' diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ea1dc6ccf6..29c1f86072 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://gradleproxy:gradleproxy@software.r3.com/artifactory/gradle-proxy/gradle-5.4.1-all.zip +distributionUrl=https\://gradleproxy:gradleproxy@software.r3.com/artifactory/gradle-proxy/gradle-5.6.4-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/node/build.gradle b/node/build.gradle index db3cc95796..99fcb9760a 100644 --- a/node/build.gradle +++ b/node/build.gradle @@ -20,6 +20,11 @@ ext { jolokia_version = constants.getProperty('jolokiaAgentVersion') } +evaluationDependsOn(':core-deterministic') +evaluationDependsOn(':serialization-deterministic') +evaluationDependsOn(':serialization-djvm:deserializers') +evaluationDependsOn(':node:djvm') + //noinspection GroovyAssignabilityCheck configurations { integrationTestCompile.extendsFrom testCompile @@ -256,13 +261,13 @@ tasks.withType(Test).configureEach { systemProperty 'deterministic-sources.path', configurations.deterministic.asPath } -task integrationTest(type: Test) { +tasks.register('integrationTest', Test) { testClassesDirs = sourceSets.integrationTest.output.classesDirs classpath = sourceSets.integrationTest.runtimeClasspath maxParallelForks = (System.env.CORDA_NODE_INT_TESTING_FORKS == null) ? 1 : "$System.env.CORDA_NODE_INT_TESTING_FORKS".toInteger() } -task slowIntegrationTest(type: Test) { +tasks.register('slowIntegrationTest', Test) { testClassesDirs = sourceSets.slowIntegrationTest.output.classesDirs classpath = sourceSets.slowIntegrationTest.runtimeClasspath maxParallelForks = 1 @@ -320,7 +325,7 @@ publish { name jar.baseName } -test { +tasks.named('test', Test) { maxHeapSize = "3g" maxParallelForks = (System.env.CORDA_NODE_TESTING_FORKS == null) ? 1 : "$System.env.CORDA_NODE_TESTING_FORKS".toInteger() } From 2204f443324babb68155627a62156e1bc5a51af7 Mon Sep 17 00:00:00 2001 From: LankyDan Date: Wed, 8 Jul 2020 14:41:29 +0100 Subject: [PATCH 59/85] ENT-5196 handle errors during flow initialisation (#6378) Changes to `TopLevelTransition` after merging from earlier releases. When a flow is kept for observation and its checkpoint is saved as HOSPITALIZED in the database, we must acknowledge the session init and flow start events so that they are not replayed on node startup. Otherwise the same flow will be ran twice when the node is restarted, one from the checkpoint and one from artemis. --- .../transitions/TopLevelTransition.kt | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/transitions/TopLevelTransition.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/transitions/TopLevelTransition.kt index 887041b82c..1b7d79dfec 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/transitions/TopLevelTransition.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/transitions/TopLevelTransition.kt @@ -3,7 +3,9 @@ package net.corda.node.services.statemachine.transitions import net.corda.core.crypto.SecureHash import net.corda.core.flows.InitiatingFlow import net.corda.core.internal.FlowIORequest +import net.corda.core.serialization.deserialize import net.corda.core.utilities.Try +import net.corda.node.services.messaging.DeduplicationHandler import net.corda.node.services.statemachine.Action import net.corda.node.services.statemachine.Checkpoint import net.corda.node.services.statemachine.DeduplicationId @@ -11,12 +13,15 @@ import net.corda.node.services.statemachine.EndSessionMessage import net.corda.node.services.statemachine.ErrorState import net.corda.node.services.statemachine.Event import net.corda.node.services.statemachine.ExistingSessionMessage +import net.corda.node.services.statemachine.ExternalEvent import net.corda.node.services.statemachine.FlowRemovalReason import net.corda.node.services.statemachine.FlowSessionImpl import net.corda.node.services.statemachine.FlowState +import net.corda.node.services.statemachine.InitialSessionMessage import net.corda.node.services.statemachine.InitiatedSessionState import net.corda.node.services.statemachine.SenderDeduplicationId import net.corda.node.services.statemachine.SessionId +import net.corda.node.services.statemachine.SessionMessage import net.corda.node.services.statemachine.SessionState import net.corda.node.services.statemachine.StateMachineState import net.corda.node.services.statemachine.SubFlow @@ -321,15 +326,33 @@ class TopLevelTransition( private fun overnightObservationTransition(): TransitionResult { return builder { + val flowStartEvents = currentState.pendingDeduplicationHandlers.filter(::isFlowStartEvent) val newCheckpoint = startingState.checkpoint.copy(status = Checkpoint.FlowStatus.HOSPITALIZED) - actions.add(Action.CreateTransaction) - actions.add(Action.PersistCheckpoint(context.id, newCheckpoint, isCheckpointUpdate = currentState.isAnyCheckpointPersisted)) - actions.add(Action.CommitTransaction) - currentState = currentState.copy(checkpoint = newCheckpoint) + actions += Action.CreateTransaction + actions += Action.PersistDeduplicationFacts(flowStartEvents) + actions += Action.PersistCheckpoint(context.id, newCheckpoint, isCheckpointUpdate = currentState.isAnyCheckpointPersisted) + actions += Action.CommitTransaction + actions += Action.AcknowledgeMessages(flowStartEvents) + currentState = currentState.copy( + checkpoint = startingState.checkpoint.copy(status = Checkpoint.FlowStatus.HOSPITALIZED), + pendingDeduplicationHandlers = currentState.pendingDeduplicationHandlers - flowStartEvents + ) FlowContinuation.ProcessEvents } } + private fun isFlowStartEvent(handler: DeduplicationHandler): Boolean { + return handler.externalCause.run { isSessionInit() || isFlowStart() } + } + + private fun ExternalEvent.isSessionInit(): Boolean { + return this is ExternalEvent.ExternalMessageEvent && this.receivedMessage.data.deserialize() is InitialSessionMessage + } + + private fun ExternalEvent.isFlowStart(): Boolean { + return this is ExternalEvent.ExternalStartFlowEvent<*> + } + private fun wakeUpFromSleepTransition(): TransitionResult { return builder { resumeFlowLogic(Unit) From b05c0f0cc1ed47df9a12b2ac975ab1976e92f31e Mon Sep 17 00:00:00 2001 From: LankyDan Date: Wed, 8 Jul 2020 14:43:24 +0100 Subject: [PATCH 60/85] ENT-5196 handle errors during flow initialisation (#6378) Changes to `StaffedFlowHospital` after merging from earlier releases. --- .../corda/node/services/statemachine/StaffedFlowHospital.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/StaffedFlowHospital.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/StaffedFlowHospital.kt index ae5434e666..ff83f34653 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/StaffedFlowHospital.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/StaffedFlowHospital.kt @@ -355,7 +355,7 @@ class StaffedFlowHospital(private val flowMessaging: FlowMessaging, } fun timesResuscitated(currentState: StateMachineState): Int { - val lastAdmittanceSuspendCount = currentState.checkpoint.numberOfSuspends + val lastAdmittanceSuspendCount = currentState.checkpoint.checkpointState.numberOfSuspends return records.count { ResuscitationSpecialist in it.by && it.suspendCount == lastAdmittanceSuspendCount } } @@ -392,9 +392,9 @@ class StaffedFlowHospital(private val flowMessaging: FlowMessaging, } } - enum class Outcome(val event: Event?) { + enum class Outcome(val event: Event) { DISCHARGE(Event.RetryFlowFromSafePoint), - OVERNIGHT_OBSERVATION(null), + OVERNIGHT_OBSERVATION(Event.OvernightObservation), UNTREATABLE(Event.StartErrorPropagation) } From b6d649634fef6b5b7065f28e41c551430b1c482c Mon Sep 17 00:00:00 2001 From: LankyDan Date: Wed, 8 Jul 2020 16:02:17 +0100 Subject: [PATCH 61/85] ENT-5196 handle errors during flow initialisation (#6378) Update flow error handling tests after merging from earlier releases --- .../StateMachineErrorHandlingTest.kt | 74 ++++++++++--------- .../StateMachineFinalityErrorHandlingTest.kt | 19 +++-- .../StateMachineFlowInitErrorHandlingTest.kt | 68 ++++++++--------- .../StateMachineGeneralErrorHandlingTest.kt | 68 ++++++++--------- .../StateMachineKillFlowErrorHandlingTest.kt | 10 +-- .../StateMachineSubFlowErrorHandlingTest.kt | 8 +- .../StatemachineGeneralErrorHandlingTest.kt | 0 .../StatemachineKillFlowErrorHandlingTest.kt | 0 8 files changed, 127 insertions(+), 120 deletions(-) delete mode 100644 node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineGeneralErrorHandlingTest.kt delete mode 100644 node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineKillFlowErrorHandlingTest.kt diff --git a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineErrorHandlingTest.kt b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineErrorHandlingTest.kt index 3afb279281..8233cc79df 100644 --- a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineErrorHandlingTest.kt +++ b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineErrorHandlingTest.kt @@ -127,10 +127,21 @@ abstract class StateMachineErrorHandlingTest { internal fun CordaRPCOps.assertHospitalCountsAllZero() = assertHospitalCounts() - internal fun CordaRPCOps.assertNumberOfCheckpoints(number: Long) { - assertEquals(number, startFlow(StateMachineErrorHandlingTest::GetNumberOfCheckpointsFlow).returnValue.get()) + internal fun CordaRPCOps.assertNumberOfCheckpoints( + runnable: Int = 0, + failed: Int = 0, + completed: Int = 0, + hospitalized: Int = 0 + ) { + val counts = startFlow(StateMachineErrorHandlingTest::GetNumberOfCheckpointsFlow).returnValue.getOrThrow(20.seconds) + assertEquals(runnable, counts.runnable, "There should be $runnable runnable checkpoints") + assertEquals(failed, counts.failed, "There should be $failed failed checkpoints") + assertEquals(completed, counts.completed, "There should be $completed completed checkpoints") + assertEquals(hospitalized, counts.hospitalized, "There should be $hospitalized hospitalized checkpoints") } + internal fun CordaRPCOps.assertNumberOfCheckpointsAllZero() = assertNumberOfCheckpoints() + @StartableByRPC @InitiatingFlow class SendAMessageFlow(private val party: Party) : FlowLogic() { @@ -181,44 +192,37 @@ abstract class StateMachineErrorHandlingTest { } @StartableByRPC - class ThrowAnErrorFlow : FlowLogic() { - @Suspendable - override fun call(): String { - throwException() - return "cant get here" - } + class GetNumberOfCheckpointsFlow : FlowLogic() { + override fun call() = NumberOfCheckpoints( + runnable = getNumberOfCheckpointsWithStatus(Checkpoint.FlowStatus.RUNNABLE), + failed = getNumberOfCheckpointsWithStatus(Checkpoint.FlowStatus.FAILED), + completed = getNumberOfCheckpointsWithStatus(Checkpoint.FlowStatus.COMPLETED), + hospitalized = getNumberOfCheckpointsWithStatus(Checkpoint.FlowStatus.HOSPITALIZED) + ) - private fun throwException() { - logger.info("Throwing exception in flow") - throw IllegalStateException("throwing exception in flow") - } - } - - @StartableByRPC - class GetNumberOfUncompletedCheckpointsFlow : FlowLogic() { - override fun call(): Long { - val sqlStatement = "select count(*) from node_checkpoints where status not in (${Checkpoint.FlowStatus.COMPLETED.ordinal})" - return serviceHub.jdbcSession().prepareStatement(sqlStatement).use { ps -> - ps.executeQuery().use { rs -> - rs.next() - rs.getLong(1) + private fun getNumberOfCheckpointsWithStatus(status: Checkpoint.FlowStatus): Int { + return serviceHub.jdbcSession() + .prepareStatement("select count(*) from node_checkpoints where status = ? and flow_id != ?") + .apply { + setInt(1, status.ordinal) + setString(2, runId.uuid.toString()) } - } + .use { ps -> + ps.executeQuery().use { rs -> + rs.next() + rs.getLong(1) + } + }.toInt() } } - @StartableByRPC - class GetNumberOfHospitalizedCheckpointsFlow : FlowLogic() { - override fun call(): Long { - val sqlStatement = "select count(*) from node_checkpoints where status in (${Checkpoint.FlowStatus.HOSPITALIZED.ordinal})" - return serviceHub.jdbcSession().prepareStatement(sqlStatement).use { ps -> - ps.executeQuery().use { rs -> - rs.next() - rs.getLong(1) - } - } - } - } + @CordaSerializable + data class NumberOfCheckpoints( + val runnable: Int = 0, + val failed: Int = 0, + val completed: Int = 0, + val hospitalized: Int = 0 + ) // Internal use for testing only!! @StartableByRPC diff --git a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineFinalityErrorHandlingTest.kt b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineFinalityErrorHandlingTest.kt index 4207e39dca..0613fd277e 100644 --- a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineFinalityErrorHandlingTest.kt +++ b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineFinalityErrorHandlingTest.kt @@ -77,11 +77,11 @@ class StateMachineFinalityErrorHandlingTest : StateMachineErrorHandlingTest() { defaultNotaryIdentity ).returnValue.getOrThrow(30.seconds) + alice.rpc.assertNumberOfCheckpointsAllZero() + charlie.rpc.assertNumberOfCheckpoints(hospitalized = 1) charlie.rpc.assertHospitalCounts(observation = 1) assertEquals(0, alice.rpc.stateMachinesSnapshot().size) assertEquals(1, charlie.rpc.stateMachinesSnapshot().size) - alice.rpc.assertNumberOfCheckpoints(0) - charlie.rpc.assertNumberOfCheckpoints(1) } } @@ -139,11 +139,11 @@ class StateMachineFinalityErrorHandlingTest : StateMachineErrorHandlingTest() { defaultNotaryIdentity ).returnValue.getOrThrow(30.seconds) + alice.rpc.assertNumberOfCheckpointsAllZero() + charlie.rpc.assertNumberOfCheckpoints(hospitalized = 1) charlie.rpc.assertHospitalCounts(observation = 1) assertEquals(0, alice.rpc.stateMachinesSnapshot().size) assertEquals(1, charlie.rpc.stateMachinesSnapshot().size) - alice.rpc.assertNumberOfCheckpoints(0) - charlie.rpc.assertNumberOfCheckpoints(1) } } @@ -201,11 +201,14 @@ class StateMachineFinalityErrorHandlingTest : StateMachineErrorHandlingTest() { defaultNotaryIdentity ).returnValue.getOrThrow(30.seconds) + // This sleep is a bit suspect... + Thread.sleep(1000) + + alice.rpc.assertNumberOfCheckpointsAllZero() + charlie.rpc.assertNumberOfCheckpointsAllZero() charlie.rpc.assertHospitalCounts(discharged = 3) assertEquals(0, alice.rpc.stateMachinesSnapshot().size) assertEquals(0, charlie.rpc.stateMachinesSnapshot().size) - alice.rpc.assertNumberOfCheckpoints(0) - charlie.rpc.assertNumberOfCheckpoints(0) } } @@ -268,14 +271,14 @@ class StateMachineFinalityErrorHandlingTest : StateMachineErrorHandlingTest() { ).returnValue.getOrThrow(30.seconds) } + alice.rpc.assertNumberOfCheckpoints(runnable = 1) + charlie.rpc.assertNumberOfCheckpoints(hospitalized = 1) charlie.rpc.assertHospitalCounts( discharged = 3, observation = 1 ) assertEquals(1, alice.rpc.stateMachinesSnapshot().size) assertEquals(1, charlie.rpc.stateMachinesSnapshot().size) - alice.rpc.assertNumberOfCheckpoints(1) - charlie.rpc.assertNumberOfCheckpoints(1) } } } \ No newline at end of file diff --git a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineFlowInitErrorHandlingTest.kt b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineFlowInitErrorHandlingTest.kt index 9f3e0a5b66..c36d9750f0 100644 --- a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineFlowInitErrorHandlingTest.kt +++ b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineFlowInitErrorHandlingTest.kt @@ -45,7 +45,7 @@ class StateMachineFlowInitErrorHandlingTest : StateMachineErrorHandlingTest() { val rules = """ RULE Create Counter - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF createCounter("counter", $counter) @@ -53,7 +53,7 @@ class StateMachineFlowInitErrorHandlingTest : StateMachineErrorHandlingTest() { ENDRULE RULE Throw exception on executeCommitTransaction action - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF readCounter("counter") < 3 @@ -70,9 +70,9 @@ class StateMachineFlowInitErrorHandlingTest : StateMachineErrorHandlingTest() { 30.seconds ) + alice.rpc.assertNumberOfCheckpointsAllZero() alice.rpc.assertHospitalCounts(discharged = 3) assertEquals(0, alice.rpc.stateMachinesSnapshot().size) - alice.rpc.assertNumberOfCheckpoints(0) } } @@ -117,9 +117,9 @@ class StateMachineFlowInitErrorHandlingTest : StateMachineErrorHandlingTest() { ).returnValue.getOrThrow(30.seconds) } + alice.rpc.assertNumberOfCheckpoints(failed = 1) alice.rpc.assertHospitalCounts(propagated = 1) assertEquals(0, alice.rpc.stateMachinesSnapshot().size) - alice.rpc.assertNumberOfCheckpoints(0) } } @@ -139,7 +139,7 @@ class StateMachineFlowInitErrorHandlingTest : StateMachineErrorHandlingTest() { val rules = """ RULE Create Counter - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF createCounter("counter", $counter) @@ -147,7 +147,7 @@ class StateMachineFlowInitErrorHandlingTest : StateMachineErrorHandlingTest() { ENDRULE RULE Throw exception on executeCommitTransaction action - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF readCounter("counter") == 0 @@ -170,9 +170,9 @@ class StateMachineFlowInitErrorHandlingTest : StateMachineErrorHandlingTest() { charlie.nodeInfo.singleIdentity() ).returnValue.getOrThrow(30.seconds) + alice.rpc.assertNumberOfCheckpointsAllZero() alice.rpc.assertHospitalCounts(discharged = 1) assertEquals(0, alice.rpc.stateMachinesSnapshot().size) - alice.rpc.assertNumberOfCheckpoints(0) } } @@ -192,7 +192,7 @@ class StateMachineFlowInitErrorHandlingTest : StateMachineErrorHandlingTest() { val rules = """ RULE Create Counter - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF createCounter("counter", $counter) @@ -200,7 +200,7 @@ class StateMachineFlowInitErrorHandlingTest : StateMachineErrorHandlingTest() { ENDRULE RULE Throw exception on executeCommitTransaction action - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF readCounter("counter") == 0 @@ -223,9 +223,9 @@ class StateMachineFlowInitErrorHandlingTest : StateMachineErrorHandlingTest() { charlie.nodeInfo.singleIdentity() ).returnValue.getOrThrow(30.seconds) + alice.rpc.assertNumberOfCheckpointsAllZero() alice.rpc.assertHospitalCounts(discharged = 1) assertEquals(0, alice.rpc.stateMachinesSnapshot().size) - alice.rpc.assertNumberOfCheckpoints(0) } } @@ -247,7 +247,7 @@ class StateMachineFlowInitErrorHandlingTest : StateMachineErrorHandlingTest() { val rules = """ RULE Create Counter - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF createCounter("counter", $counter) @@ -255,7 +255,7 @@ class StateMachineFlowInitErrorHandlingTest : StateMachineErrorHandlingTest() { ENDRULE RULE Throw exception on executeCommitTransaction action - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF readCounter("counter") < 4 @@ -272,17 +272,17 @@ class StateMachineFlowInitErrorHandlingTest : StateMachineErrorHandlingTest() { // flow is not signaled as started calls to [getOrThrow] will hang, sleeping instead Thread.sleep(30.seconds.toMillis()) + alice.rpc.assertNumberOfCheckpoints(hospitalized = 1) alice.rpc.assertHospitalCounts( discharged = 3, observation = 1 ) assertEquals(1, alice.rpc.stateMachinesSnapshot().size) - alice.rpc.assertNumberOfCheckpoints(0) val terminated = (alice as OutOfProcessImpl).stop(60.seconds) assertTrue(terminated, "The node must be shutdown before it can be restarted") val (alice2, _) = createBytemanNode(ALICE_NAME) - Thread.sleep(10.seconds.toMillis()) - alice2.rpc.assertNumberOfCheckpoints(0) + Thread.sleep(20.seconds.toMillis()) + alice2.rpc.assertNumberOfCheckpointsAllZero() } } @@ -303,7 +303,7 @@ class StateMachineFlowInitErrorHandlingTest : StateMachineErrorHandlingTest() { val rules = """ RULE Throw exception on executeCommitTransaction action after first suspend + commit - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF !flagged("commit_exception_flag") @@ -311,7 +311,7 @@ class StateMachineFlowInitErrorHandlingTest : StateMachineErrorHandlingTest() { ENDRULE RULE Throw exception on retry - CLASS ${SingleThreadedStateMachineManager::class.java.name} + CLASS $stateMachineManagerClassName METHOD onExternalStartFlow AT ENTRY IF flagged("commit_exception_flag") && !flagged("retry_exception_flag") @@ -328,12 +328,12 @@ class StateMachineFlowInitErrorHandlingTest : StateMachineErrorHandlingTest() { 30.seconds ) + alice.rpc.assertNumberOfCheckpointsAllZero() alice.rpc.assertHospitalCounts( discharged = 1, dischargedRetry = 1 ) assertEquals(0, alice.rpc.stateMachinesSnapshot().size) - alice.rpc.assertNumberOfCheckpoints(0) } } @@ -356,7 +356,7 @@ class StateMachineFlowInitErrorHandlingTest : StateMachineErrorHandlingTest() { val rules = """ RULE Create Counter - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF createCounter("counter", $counter) @@ -364,7 +364,7 @@ class StateMachineFlowInitErrorHandlingTest : StateMachineErrorHandlingTest() { ENDRULE RULE Throw exception on executeCommitTransaction action - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF readCounter("counter") < 3 @@ -381,9 +381,9 @@ class StateMachineFlowInitErrorHandlingTest : StateMachineErrorHandlingTest() { 30.seconds ) + alice.rpc.assertNumberOfCheckpointsAllZero() + charlie.rpc.assertNumberOfCheckpointsAllZero() charlie.rpc.assertHospitalCounts(discharged = 3) - alice.rpc.assertNumberOfCheckpoints(0) - charlie.rpc.assertNumberOfCheckpoints(0) } } @@ -405,7 +405,7 @@ class StateMachineFlowInitErrorHandlingTest : StateMachineErrorHandlingTest() { val rules = """ RULE Create Counter - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF createCounter("counter", $counter) @@ -413,7 +413,7 @@ class StateMachineFlowInitErrorHandlingTest : StateMachineErrorHandlingTest() { ENDRULE RULE Throw exception on executeCommitTransaction action - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF readCounter("counter") < 4 @@ -430,20 +430,20 @@ class StateMachineFlowInitErrorHandlingTest : StateMachineErrorHandlingTest() { // flow is not signaled as started calls to [getOrThrow] will hang, sleeping instead Thread.sleep(30.seconds.toMillis()) + alice.rpc.assertNumberOfCheckpoints(runnable = 1) + charlie.rpc.assertNumberOfCheckpoints(hospitalized = 1) charlie.rpc.assertHospitalCounts( discharged = 3, observation = 1 ) assertEquals(1, alice.rpc.stateMachinesSnapshot().size) assertEquals(1, charlie.rpc.stateMachinesSnapshot().size) - alice.rpc.assertNumberOfCheckpoints(1) - charlie.rpc.assertNumberOfCheckpoints(0) val terminated = (charlie as OutOfProcessImpl).stop(60.seconds) assertTrue(terminated, "The node must be shutdown before it can be restarted") val (charlie2, _) = createBytemanNode(CHARLIE_NAME) Thread.sleep(10.seconds.toMillis()) - alice.rpc.assertNumberOfCheckpoints(0) - charlie2.rpc.assertNumberOfCheckpoints(0) + alice.rpc.assertNumberOfCheckpointsAllZero() + charlie2.rpc.assertNumberOfCheckpointsAllZero() } } @@ -469,7 +469,7 @@ class StateMachineFlowInitErrorHandlingTest : StateMachineErrorHandlingTest() { val rules = """ RULE Create Counter - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF createCounter("counter", $counter) @@ -477,7 +477,7 @@ class StateMachineFlowInitErrorHandlingTest : StateMachineErrorHandlingTest() { ENDRULE RULE Throw exception on executeCommitTransaction action - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF readCounter("counter") < 3 @@ -502,13 +502,13 @@ class StateMachineFlowInitErrorHandlingTest : StateMachineErrorHandlingTest() { 30.seconds ) + alice.rpc.assertNumberOfCheckpointsAllZero() charlie.rpc.assertHospitalCounts( discharged = 3, observation = 0 ) assertEquals(0, alice.rpc.stateMachinesSnapshot().size) assertEquals(0, charlie.rpc.stateMachinesSnapshot().size) - assertEquals(0, charlie.rpc.startFlow(StateMachineErrorHandlingTest::GetNumberOfCheckpointsFlow).returnValue.get()) } } @@ -534,7 +534,7 @@ class StateMachineFlowInitErrorHandlingTest : StateMachineErrorHandlingTest() { val rules = """ RULE Create Counter - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF createCounter("counter", $counter) @@ -542,7 +542,7 @@ class StateMachineFlowInitErrorHandlingTest : StateMachineErrorHandlingTest() { ENDRULE RULE Throw exception on executeCommitTransaction action - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF readCounter("counter") < 4 @@ -569,13 +569,13 @@ class StateMachineFlowInitErrorHandlingTest : StateMachineErrorHandlingTest() { ) } + charlie.rpc.assertNumberOfCheckpoints(hospitalized = 1) charlie.rpc.assertHospitalCounts( discharged = 3, observation = 1 ) assertEquals(1, alice.rpc.stateMachinesSnapshot().size) assertEquals(1, charlie.rpc.stateMachinesSnapshot().size) - assertEquals(0, charlie.rpc.startFlow(StateMachineErrorHandlingTest::GetNumberOfCheckpointsFlow).returnValue.get()) } } } \ No newline at end of file diff --git a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineGeneralErrorHandlingTest.kt b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineGeneralErrorHandlingTest.kt index 8e3f44597a..c1af1bce1a 100644 --- a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineGeneralErrorHandlingTest.kt +++ b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineGeneralErrorHandlingTest.kt @@ -40,7 +40,7 @@ class StateMachineGeneralErrorHandlingTest : StateMachineErrorHandlingTest() { val rules = """ RULE Create Counter - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeSendMultiple AT ENTRY IF createCounter("counter", $counter) @@ -48,7 +48,7 @@ class StateMachineGeneralErrorHandlingTest : StateMachineErrorHandlingTest() { ENDRULE RULE Throw exception on executeSendMultiple action - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeSendMultiple AT ENTRY IF readCounter("counter") < 4 @@ -67,12 +67,12 @@ class StateMachineGeneralErrorHandlingTest : StateMachineErrorHandlingTest() { ) } + alice.rpc.assertNumberOfCheckpoints(hospitalized = 1) alice.rpc.assertHospitalCounts( discharged = 3, observation = 1 ) assertEquals(1, alice.rpc.stateMachinesSnapshot().size) - alice.rpc.assertNumberOfCheckpoints(1) } } @@ -92,7 +92,7 @@ class StateMachineGeneralErrorHandlingTest : StateMachineErrorHandlingTest() { val rules = """ RULE Create Counter - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeSendMultiple AT ENTRY IF createCounter("counter", $counter) @@ -100,7 +100,7 @@ class StateMachineGeneralErrorHandlingTest : StateMachineErrorHandlingTest() { ENDRULE RULE Throw exception on executeSendMultiple action - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeSendMultiple AT ENTRY IF readCounter("counter") < 3 @@ -117,9 +117,9 @@ class StateMachineGeneralErrorHandlingTest : StateMachineErrorHandlingTest() { 30.seconds ) + alice.rpc.assertNumberOfCheckpointsAllZero() alice.rpc.assertHospitalCounts(discharged = 3) assertEquals(0, alice.rpc.stateMachinesSnapshot().size) - alice.rpc.assertNumberOfCheckpoints(0) } } @@ -140,7 +140,7 @@ class StateMachineGeneralErrorHandlingTest : StateMachineErrorHandlingTest() { val rules = """ RULE Set flag when inside executeAcknowledgeMessages - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeAcknowledgeMessages AT INVOKE ${DeduplicationHandler::class.java.name}.afterDatabaseTransaction() IF !flagged("exception_flag") @@ -165,9 +165,9 @@ class StateMachineGeneralErrorHandlingTest : StateMachineErrorHandlingTest() { 30.seconds ) + alice.rpc.assertNumberOfCheckpointsAllZero() alice.rpc.assertHospitalCountsAllZero() assertEquals(0, alice.rpc.stateMachinesSnapshot().size) - alice.rpc.assertNumberOfCheckpoints(0) } } @@ -195,7 +195,7 @@ class StateMachineGeneralErrorHandlingTest : StateMachineErrorHandlingTest() { ENDRULE RULE Throw exception on executeCommitTransaction action - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF flagged("my_flag") && readCounter("counter") < 3 @@ -209,12 +209,12 @@ class StateMachineGeneralErrorHandlingTest : StateMachineErrorHandlingTest() { alice.rpc.startFlow(StateMachineErrorHandlingTest::ThrowAnErrorFlow).returnValue.getOrThrow(60.seconds) } + alice.rpc.assertNumberOfCheckpoints(failed = 1) alice.rpc.assertHospitalCounts( propagated = 1, propagatedRetry = 3 ) assertEquals(0, alice.rpc.stateMachinesSnapshot().size) - alice.rpc.assertNumberOfCheckpoints(0) } } @@ -243,7 +243,7 @@ class StateMachineGeneralErrorHandlingTest : StateMachineErrorHandlingTest() { ENDRULE RULE Throw exception on executeCommitTransaction action after first suspend + commit - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF flagged("suspend_flag") && flagged("commit_flag") && !flagged("commit_exception_flag") @@ -251,7 +251,7 @@ class StateMachineGeneralErrorHandlingTest : StateMachineErrorHandlingTest() { ENDRULE RULE Set flag when executing first commit - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF flagged("suspend_flag") && !flagged("commit_flag") @@ -259,7 +259,7 @@ class StateMachineGeneralErrorHandlingTest : StateMachineErrorHandlingTest() { ENDRULE RULE Throw exception on retry - CLASS ${SingleThreadedStateMachineManager::class.java.name} + CLASS $stateMachineManagerClassName METHOD addAndStartFlow AT ENTRY IF flagged("suspend_flag") && flagged("commit_flag") && !flagged("retry_exception_flag") @@ -274,12 +274,12 @@ class StateMachineGeneralErrorHandlingTest : StateMachineErrorHandlingTest() { charlie.nodeInfo.singleIdentity() ).returnValue.getOrThrow(40.seconds) + alice.rpc.assertNumberOfCheckpointsAllZero() alice.rpc.assertHospitalCounts( discharged = 1, dischargedRetry = 1 ) assertEquals(0, alice.rpc.stateMachinesSnapshot().size) - alice.rpc.assertNumberOfCheckpoints(0) } } @@ -302,7 +302,7 @@ class StateMachineGeneralErrorHandlingTest : StateMachineErrorHandlingTest() { // seems to be restarting the flow from the beginning every time val rules = """ RULE Create Counter - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF createCounter("counter", $counter) @@ -318,7 +318,7 @@ class StateMachineGeneralErrorHandlingTest : StateMachineErrorHandlingTest() { ENDRULE RULE Throw exception on executeCommitTransaction action after first suspend + commit - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF flagged("suspend_flag") && flagged("commit_flag") && readCounter("counter") < 3 @@ -326,7 +326,7 @@ class StateMachineGeneralErrorHandlingTest : StateMachineErrorHandlingTest() { ENDRULE RULE Set flag when executing first commit - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF flagged("suspend_flag") && !flagged("commit_flag") @@ -343,9 +343,9 @@ class StateMachineGeneralErrorHandlingTest : StateMachineErrorHandlingTest() { 30.seconds ) + alice.rpc.assertNumberOfCheckpointsAllZero() alice.rpc.assertHospitalCounts(discharged = 3) assertEquals(0, alice.rpc.stateMachinesSnapshot().size) - alice.rpc.assertNumberOfCheckpoints(0) } } @@ -368,7 +368,7 @@ class StateMachineGeneralErrorHandlingTest : StateMachineErrorHandlingTest() { // seems to be restarting the flow from the beginning every time val rules = """ RULE Create Counter - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF createCounter("counter", $counter) @@ -384,7 +384,7 @@ class StateMachineGeneralErrorHandlingTest : StateMachineErrorHandlingTest() { ENDRULE RULE Throw exception on executeCommitTransaction when removing checkpoint - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF flagged("remove_checkpoint_flag") && readCounter("counter") < 3 @@ -401,9 +401,9 @@ class StateMachineGeneralErrorHandlingTest : StateMachineErrorHandlingTest() { 30.seconds ) + alice.rpc.assertNumberOfCheckpointsAllZero() alice.rpc.assertHospitalCounts(discharged = 3) assertEquals(0, alice.rpc.stateMachinesSnapshot().size) - alice.rpc.assertNumberOfCheckpoints(0) } } @@ -424,7 +424,7 @@ class StateMachineGeneralErrorHandlingTest : StateMachineErrorHandlingTest() { val rules = """ RULE Create Counter - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF createCounter("counter", $counter) @@ -440,7 +440,7 @@ class StateMachineGeneralErrorHandlingTest : StateMachineErrorHandlingTest() { ENDRULE RULE Throw exception on executeCommitTransaction when removing checkpoint - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF flagged("remove_checkpoint_flag") && readCounter("counter") < 4 @@ -462,12 +462,12 @@ class StateMachineGeneralErrorHandlingTest : StateMachineErrorHandlingTest() { ) } + alice.rpc.assertNumberOfCheckpoints(hospitalized = 1) alice.rpc.assertHospitalCounts( discharged = 3, observation = 1 ) assertEquals(1, alice.rpc.stateMachinesSnapshot().size) - alice.rpc.assertNumberOfCheckpoints(1) } } @@ -493,7 +493,7 @@ class StateMachineGeneralErrorHandlingTest : StateMachineErrorHandlingTest() { val rules = """ RULE Create Counter - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF createCounter("counter", $counter) @@ -501,7 +501,7 @@ class StateMachineGeneralErrorHandlingTest : StateMachineErrorHandlingTest() { ENDRULE RULE Throw exception on executeCommitTransaction action - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF readCounter("counter") < 3 @@ -526,12 +526,12 @@ class StateMachineGeneralErrorHandlingTest : StateMachineErrorHandlingTest() { 30.seconds ) + alice.rpc.assertNumberOfCheckpointsAllZero() alice.rpc.assertHospitalCounts( discharged = 3, observation = 0 ) assertEquals(0, alice.rpc.stateMachinesSnapshot().size) - assertEquals(0, alice.rpc.startFlow(StateMachineErrorHandlingTest::GetNumberOfCheckpointsFlow).returnValue.get()) } } @@ -557,7 +557,7 @@ class StateMachineGeneralErrorHandlingTest : StateMachineErrorHandlingTest() { val rules = """ RULE Create Counter - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF createCounter("counter", $counter) @@ -565,7 +565,7 @@ class StateMachineGeneralErrorHandlingTest : StateMachineErrorHandlingTest() { ENDRULE RULE Throw exception on executeCommitTransaction action - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF readCounter("counter") < 4 @@ -590,12 +590,12 @@ class StateMachineGeneralErrorHandlingTest : StateMachineErrorHandlingTest() { // flow is not signaled as started calls to [getOrThrow] will hang, sleeping instead Thread.sleep(30.seconds.toMillis()) + alice.rpc.assertNumberOfCheckpoints(hospitalized = 1) alice.rpc.assertHospitalCounts( discharged = 3, observation = 1 ) assertEquals(1, alice.rpc.stateMachinesSnapshot().size) - assertEquals(0, alice.rpc.startFlow(StateMachineErrorHandlingTest::GetNumberOfCheckpointsFlow).returnValue.get()) } } @@ -615,7 +615,7 @@ class StateMachineGeneralErrorHandlingTest : StateMachineErrorHandlingTest() { val rules = """ RULE Create Counter - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF createCounter("counter", $counter) @@ -631,7 +631,7 @@ class StateMachineGeneralErrorHandlingTest : StateMachineErrorHandlingTest() { ENDRULE RULE Throw exception on executeCommitTransaction when removing checkpoint - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeCommitTransaction AT ENTRY IF flagged("remove_checkpoint_flag") && readCounter("counter") < 3 @@ -651,11 +651,11 @@ class StateMachineGeneralErrorHandlingTest : StateMachineErrorHandlingTest() { 30.seconds ) + alice.rpc.assertNumberOfCheckpointsAllZero() + charlie.rpc.assertNumberOfCheckpointsAllZero() charlie.rpc.assertHospitalCounts(discharged = 3) assertEquals(0, alice.rpc.stateMachinesSnapshot().size) assertEquals(0, charlie.rpc.stateMachinesSnapshot().size) - alice.rpc.assertNumberOfCheckpoints(0) - charlie.rpc.assertNumberOfCheckpoints(0) } } } \ No newline at end of file diff --git a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineKillFlowErrorHandlingTest.kt b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineKillFlowErrorHandlingTest.kt index 31f73c526d..ee5699456d 100644 --- a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineKillFlowErrorHandlingTest.kt +++ b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineKillFlowErrorHandlingTest.kt @@ -48,9 +48,9 @@ class StateMachineKillFlowErrorHandlingTest : StateMachineErrorHandlingTest() { assertFailsWith { flow.returnValue.getOrThrow(20.seconds) } assertTrue(flowKilled) + alice.rpc.assertNumberOfCheckpointsAllZero() alice.rpc.assertHospitalCountsAllZero() assertEquals(0, alice.rpc.stateMachinesSnapshot().size) - alice.rpc.assertNumberOfCheckpoints(0) } } @@ -85,9 +85,9 @@ class StateMachineKillFlowErrorHandlingTest : StateMachineErrorHandlingTest() { assertFailsWith { flow.returnValue.getOrThrow(30.seconds) } assertTrue(flowKilled) + alice.rpc.assertNumberOfCheckpointsAllZero() alice.rpc.assertHospitalCountsAllZero() assertEquals(0, alice.rpc.stateMachinesSnapshot().size) - alice.rpc.assertNumberOfCheckpoints(0) } } @@ -108,7 +108,7 @@ class StateMachineKillFlowErrorHandlingTest : StateMachineErrorHandlingTest() { val rules = """ RULE Create Counter - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeSendMultiple AT ENTRY IF createCounter("counter", $counter) @@ -116,7 +116,7 @@ class StateMachineKillFlowErrorHandlingTest : StateMachineErrorHandlingTest() { ENDRULE RULE Throw exception on executeSendMultiple action - CLASS ${ActionExecutorImpl::class.java.name} + CLASS $actionExecutorClassName METHOD executeSendMultiple AT ENTRY IF readCounter("counter") < 4 @@ -132,12 +132,12 @@ class StateMachineKillFlowErrorHandlingTest : StateMachineErrorHandlingTest() { alice.rpc.killFlow(flow.id) + alice.rpc.assertNumberOfCheckpointsAllZero() alice.rpc.assertHospitalCounts( discharged = 3, observation = 1 ) assertEquals(0, alice.rpc.stateMachinesSnapshot().size) - alice.rpc.assertNumberOfCheckpoints(0) } } diff --git a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineSubFlowErrorHandlingTest.kt b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineSubFlowErrorHandlingTest.kt index 217138b66f..5a9335136b 100644 --- a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineSubFlowErrorHandlingTest.kt +++ b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineSubFlowErrorHandlingTest.kt @@ -94,9 +94,9 @@ class StateMachineSubFlowErrorHandlingTest : StateMachineErrorHandlingTest() { 30.seconds ) + alice.rpc.assertNumberOfCheckpointsAllZero() alice.rpc.assertHospitalCounts(discharged = 3) assertEquals(0, alice.rpc.stateMachinesSnapshot().size) - alice.rpc.assertNumberOfCheckpoints(0) } } @@ -165,9 +165,9 @@ class StateMachineSubFlowErrorHandlingTest : StateMachineErrorHandlingTest() { 30.seconds ) + alice.rpc.assertNumberOfCheckpointsAllZero() alice.rpc.assertHospitalCounts(discharged = 3) assertEquals(0, alice.rpc.stateMachinesSnapshot().size) - alice.rpc.assertNumberOfCheckpoints(0) } } @@ -228,9 +228,9 @@ class StateMachineSubFlowErrorHandlingTest : StateMachineErrorHandlingTest() { 30.seconds ) + alice.rpc.assertNumberOfCheckpointsAllZero() alice.rpc.assertHospitalCounts(discharged = 3) assertEquals(0, alice.rpc.stateMachinesSnapshot().size) - alice.rpc.assertNumberOfCheckpoints(0) } } @@ -299,9 +299,9 @@ class StateMachineSubFlowErrorHandlingTest : StateMachineErrorHandlingTest() { 30.seconds ) + alice.rpc.assertNumberOfCheckpointsAllZero() alice.rpc.assertHospitalCounts(discharged = 3) assertEquals(0, alice.rpc.stateMachinesSnapshot().size) - alice.rpc.assertNumberOfCheckpoints(0) } } diff --git a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineGeneralErrorHandlingTest.kt b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineGeneralErrorHandlingTest.kt deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineKillFlowErrorHandlingTest.kt b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineKillFlowErrorHandlingTest.kt deleted file mode 100644 index e69de29bb2..0000000000 From 8b231dc936519451a39d7656b45efb0a1b0c532a Mon Sep 17 00:00:00 2001 From: LankyDan Date: Wed, 8 Jul 2020 16:18:16 +0100 Subject: [PATCH 62/85] NOTICK Suppress detekt warning on `StaffedFlowHospital` --- .../net/corda/node/services/statemachine/StaffedFlowHospital.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/StaffedFlowHospital.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/StaffedFlowHospital.kt index ff83f34653..4d6e73bfbe 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/StaffedFlowHospital.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/StaffedFlowHospital.kt @@ -39,6 +39,7 @@ import kotlin.math.pow /** * This hospital consults "staff" to see if they can automatically diagnose and treat flows. */ +@Suppress("TooManyFunctions") class StaffedFlowHospital(private val flowMessaging: FlowMessaging, private val clock: Clock, private val ourSenderUUID: String) : Closeable { From 796a0178c584084aba9b98a893e8ed2aa94a34b1 Mon Sep 17 00:00:00 2001 From: Dan Newton Date: Wed, 8 Jul 2020 16:53:52 +0100 Subject: [PATCH 63/85] NOTICK Add KDoc to `CordaFuture.doOnComplete` (#6449) --- .../net/corda/core/internal/concurrent/CordaFutureImpl.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/core/src/main/kotlin/net/corda/core/internal/concurrent/CordaFutureImpl.kt b/core/src/main/kotlin/net/corda/core/internal/concurrent/CordaFutureImpl.kt index dcaceb2295..0f62fd752f 100644 --- a/core/src/main/kotlin/net/corda/core/internal/concurrent/CordaFutureImpl.kt +++ b/core/src/main/kotlin/net/corda/core/internal/concurrent/CordaFutureImpl.kt @@ -27,6 +27,12 @@ fun CordaFuture.thenMatch(success: (V) -> W, failure: (Throwabl /** When this future is done and the outcome is failure, log the throwable. */ fun CordaFuture<*>.andForget(log: Logger) = thenMatch({}, { log.error("Background task failed:", it) }) +/** + * Returns a future that will also apply the passed closure when it completes. + * + * @param accept A function to execute when completing the original future. + * @return A future returning the same result as the original future that this function was executed on. + */ fun CordaFuture.doOnComplete(accept: (RESULT) -> Unit): CordaFuture { return CordaFutureImpl().also { result -> thenMatch({ From db13e3beb95433f6c4c70f540b7e52c91fc0f5ba Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Thu, 9 Jul 2020 14:34:34 +0000 Subject: [PATCH 64/85] INFRA-456 Add codeowners (#6450) --- .github/CODEOWNERS | 62 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000000..2e77ac821f --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,62 @@ +# All documentation should be reviewed by the technical writers +*.md @corda/technical-writers + +# By default anything under core or node-api is the Kernel team +core @corda/kernel +node-api @corda/kernel +node/src/main/kotlin/net/corda/node/internal @corda/kernel +node/src/main/kotlin/net/corda/node/services @corda/kernel + +# Determinstic components +core-deterministic @chrisr3 +jdk8u-deterministic @chrisr3 +node/djvm @chrisr3 +serialization-deterministic @chrisr3 +serialization-djvm @chrisr3 +serialization-tests @chrisr3 + +# Demobench defaults to Chris, but Viktor for the main code +tools/demobench @chrisr3 +tools/demobench/src/main/kotlin/net/corda/demobench @vkolomeyko + +# General Corda code + +client/rpc @vkolomeyko + +core/src/main/kotlin/net/corda/core/flows @dimosr +core/src/main/kotlin/net/corda/core/internal/notary @thschroeter +core/src/main/kotlin/net/corda/core/messaging @vkolomeyko + +node/src/integration-test/kotlin/net/corda/node/persistence @blsemo +node/src/integration-test/kotlin/net/corda/node/services/persistence @blsemo +node/src/main/kotlin/net/corda/node/internal/artemis @rekalov +node/src/main/kotlin/net/corda/node/services/identity @rekalov +node/src/main/kotlin/net/corda/node/services/keys @rekalov +node/src/main/kotlin/net/corda/node/services/messaging @dimosr +node/src/main/kotlin/net/corda/node/services/network @rekalov +node/src/main/kotlin/net/corda/node/services/persistence @blsemo +node/src/main/kotlin/net/corda/node/services/rpc @vkolomeyko +node/src/main/kotlin/net/corda/node/services/statemachine @lankydan +node/src/main/kotlin/net/corda/node/utilities/registration @rekalov +node/src/main/kotlin/net/corda/notary @thschroeter + +node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging @vkolomeyko +node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto @rekalov +node-api/src/main/kotlin/net/corda/nodeapi/internal/cryptoservice @rekalov +node-api/src/main/kotlin/net/corda/nodeapi/internal/lifecycle @vkolomeyko +node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence @blsemo +node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper @vkolomeyko +node-api/src/test/kotlin/net/corda/nodeapi/internal/bridging @rekalov + +common/logging/src/main/kotlin/net/corda/common/logging/errorReporting @JamesHR3 +common/logging/src/test/kotlin/net/corda/commmon/logging/errorReporting @JamesHR3 + +# Single file ownerships go at the end, as they are most specific and take precedence over other ownerships + +core/src/main/kotlin/net/corda/core/internal/AbstractAttachment.kt @adelel1 +core/src/main/kotlin/net/corda/core/internal/AttachmentTrustCalculator.kt @adelel1 +core/src/main/kotlin/net/corda/core/internal/AttachmentWithContext.kt @adelel1 +core/src/main/kotlin/net/corda/core/internal/CertRole.kt @rekalov +core/src/main/kotlin/net/corda/core/node/services/AttachmentStorage.kt @adelel1 +core/src/main/kotlin/net/corda/core/node/services/IdentityService.kt @rekalov +core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt @rekalov From e684cfd1e60f52817e083254612b92be2bbed680 Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Mon, 13 Jul 2020 10:33:23 +0100 Subject: [PATCH 65/85] NOTICK: Avoid configuring more tasks in the deterministic modules. (#6452) --- core-deterministic/build.gradle | 28 +++++++++++++--------- deterministic.gradle | 2 +- jdk8u-deterministic/build.gradle | 4 +++- node/capsule/build.gradle | 6 ++--- serialization-deterministic/build.gradle | 30 ++++++++++++++---------- 5 files changed, 42 insertions(+), 28 deletions(-) diff --git a/core-deterministic/build.gradle b/core-deterministic/build.gradle index 7b90dfdd82..5be42d8084 100644 --- a/core-deterministic/build.gradle +++ b/core-deterministic/build.gradle @@ -54,8 +54,8 @@ tasks.named('jar', Jar) { enabled = false } -def coreJarTask = tasks.getByPath(':core:jar') -def originalJar = coreJarTask.outputs.files.singleFile +def coreJarTask = project(':core').tasks.named('jar', Jar) +def originalJar = coreJarTask.map { it.outputs.files.singleFile } def patchCore = tasks.register('patchCore', Zip) { dependsOn coreJarTask @@ -132,7 +132,7 @@ def jarFilter = tasks.register('jarFilter', JarFilterTask) { } } -task determinise(type: ProGuardTask) { +def determinise = tasks.register('determinise', ProGuardTask) { injars jarFilter outjars file("$buildDir/proguard/$jarBaseName-${project.version}.jar") @@ -166,17 +166,20 @@ task determinise(type: ProGuardTask) { keepclassmembers 'class net.corda.core.** { public synthetic ; }' } -task metafix(type: MetaFixerTask) { +def checkDeterminism = tasks.register('checkDeterminism', ProGuardTask) + +def metafix = tasks.register('metafix', MetaFixerTask) { outputDir file("$buildDir/libs") jars determinise suffix "" // Strip timestamps from the JAR to make it reproducible. preserveTimestamps = false + finalizedBy checkDeterminism } // DOCSTART 01 -def checkDeterminism = tasks.register('checkDeterminism', ProGuardTask) { +checkDeterminism.configure { dependsOn jdkTask injars metafix @@ -197,14 +200,17 @@ def checkDeterminism = tasks.register('checkDeterminism', ProGuardTask) { // DOCEND 01 defaultTasks "determinise" -determinise.finalizedBy metafix -metafix.finalizedBy checkDeterminism -assemble.dependsOn checkDeterminism +determinise.configure { + finalizedBy metafix +} +tasks.named('assemble') { + dependsOn checkDeterminism +} -def deterministicJar = metafix.outputs.files.singleFile +def deterministicJar = metafix.map { it.outputs.files.singleFile } artifacts { - deterministicArtifacts file: deterministicJar, name: jarBaseName, type: 'jar', extension: 'jar', builtBy: metafix - publish file: deterministicJar, name: jarBaseName, type: 'jar', extension: 'jar', builtBy: metafix + deterministicArtifacts deterministicJar + publish deterministicJar } tasks.named('sourceJar', Jar) { diff --git a/deterministic.gradle b/deterministic.gradle index 11bd05770f..751af8bfb2 100644 --- a/deterministic.gradle +++ b/deterministic.gradle @@ -11,7 +11,7 @@ evaluationDependsOn(':jdk8u-deterministic') def jdk8uDeterministic = project(':jdk8u-deterministic') ext { - jdkTask = jdk8uDeterministic.assemble + jdkTask = jdk8uDeterministic.tasks.named('assemble') deterministic_jdk_home = jdk8uDeterministic.jdk_home deterministic_rt_jar = jdk8uDeterministic.rt_jar } diff --git a/jdk8u-deterministic/build.gradle b/jdk8u-deterministic/build.gradle index 92338df169..f9a91c9cc8 100644 --- a/jdk8u-deterministic/build.gradle +++ b/jdk8u-deterministic/build.gradle @@ -37,7 +37,9 @@ def copyJdk = tasks.register('copyJdk', Copy) { } } -assemble.dependsOn copyJdk +tasks.named('assemble') { + dependsOn copyJdk +} tasks.named('jar', Jar) { enabled = false } diff --git a/node/capsule/build.gradle b/node/capsule/build.gradle index 14cddd0332..c1c4e2f4c9 100644 --- a/node/capsule/build.gradle +++ b/node/capsule/build.gradle @@ -39,9 +39,9 @@ capsule { def nodeProject = project(':node') task buildCordaJAR(type: FatCapsule, dependsOn: [ - nodeProject.tasks.jar, - project(':core-deterministic').tasks.assemble, - project(':serialization-deterministic').tasks.assemble + nodeProject.tasks.named('jar'), + project(':core-deterministic').tasks.named('assemble'), + project(':serialization-deterministic').tasks.named('assemble') ]) { applicationClass 'net.corda.node.Corda' archiveBaseName = 'corda' diff --git a/serialization-deterministic/build.gradle b/serialization-deterministic/build.gradle index 7ea284daeb..6ad42b0208 100644 --- a/serialization-deterministic/build.gradle +++ b/serialization-deterministic/build.gradle @@ -50,8 +50,8 @@ tasks.named('jar', Jar) { enabled = false } -def serializationJarTask = tasks.getByPath(':serialization:jar') -def originalJar = serializationJarTask.outputs.files.singleFile +def serializationJarTask = project(':serialization').tasks.named('jar', Jar) +def originalJar = serializationJarTask.map { it.outputs.files.singleFile } def patchSerialization = tasks.register('patchSerialization', Zip) { dependsOn serializationJarTask @@ -77,7 +77,7 @@ def patchSerialization = tasks.register('patchSerialization', Zip) { } def predeterminise = tasks.register('predeterminise', ProGuardTask) { - dependsOn project(':core-deterministic').assemble + dependsOn project(':core-deterministic').tasks.named('assemble') injars patchSerialization outjars file("$buildDir/proguard/pre-deterministic-${project.version}.jar") @@ -125,7 +125,7 @@ def jarFilter = tasks.register('jarFilter', JarFilterTask) { } } -task determinise(type: ProGuardTask) { +def determinise = tasks.register('determinise', ProGuardTask) { injars jarFilter outjars file("$buildDir/proguard/$jarBaseName-${project.version}.jar") @@ -154,16 +154,19 @@ task determinise(type: ProGuardTask) { keepclassmembers 'class net.corda.serialization.** { public synthetic ; }' } -task metafix(type: MetaFixerTask) { +def checkDeterminism = tasks.register('checkDeterminism', ProGuardTask) + +def metafix = tasks.register('metafix', MetaFixerTask) { outputDir file("$buildDir/libs") jars determinise suffix "" // Strip timestamps from the JAR to make it reproducible. preserveTimestamps = false + finalizedBy checkDeterminism } -def checkDeterminism = tasks.register('checkDeterminism', ProGuardTask) { +checkDeterminism.configure { dependsOn jdkTask injars metafix @@ -183,14 +186,17 @@ def checkDeterminism = tasks.register('checkDeterminism', ProGuardTask) { } defaultTasks "determinise" -determinise.finalizedBy metafix -metafix.finalizedBy checkDeterminism -assemble.dependsOn checkDeterminism +determinise.configure { + finalizedBy metafix +} +tasks.named('assemble') { + dependsOn checkDeterminism +} -def deterministicJar = metafix.outputs.files.singleFile +def deterministicJar = metafix.map { it.outputs.files.singleFile } artifacts { - deterministicArtifacts file: deterministicJar, name: jarBaseName, type: 'jar', extension: 'jar', builtBy: metafix - publish file: deterministicJar, name: jarBaseName, type: 'jar', extension: 'jar', builtBy: metafix + deterministicArtifacts deterministicJar + publish deterministicJar } tasks.named('sourceJar', Jar) { From 79b75ff1ec4922a1bb0e531ec818d5dd7cd68fee Mon Sep 17 00:00:00 2001 From: Yiftach Kaplan <67583323+yift-r3@users.noreply.github.com> Date: Mon, 13 Jul 2020 11:42:30 +0100 Subject: [PATCH 66/85] INFRA-429: Wait for log file to indicate that the process is running (#6433) Wait for log file to indicate that a driver node process is running, instead of trying to open a socket on the port the node is attempting to binding on. This means the driver is more responsive to the node being ready, as well as removing a race condition where the binding test could block the node from starting. As sometimes nodes do not log this information, after 20 seconds we presume the node is up. --- .../testing/node/internal/DriverDSLImpl.kt | 2 +- .../node/internal/InternalTestUtils.kt | 35 +++++++++++++------ 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt index ee55c9999c..e3822522cd 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt @@ -729,7 +729,7 @@ class DriverDSLImpl( val effectiveP2PAddress = config.corda.messagingServerAddress ?: config.corda.p2pAddress val p2pReadyFuture = nodeMustBeStartedFuture( executorService, - effectiveP2PAddress, + config.corda.baseDirectory / "net.corda.node.Corda.${identifier}.stdout.log", process ) { NodeListenProcessDeathException( diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalTestUtils.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalTestUtils.kt index 67bfb8e1b8..3c6de690bf 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalTestUtils.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalTestUtils.kt @@ -11,6 +11,7 @@ import net.corda.core.internal.FlowStateMachine import net.corda.core.internal.VisibleForTesting import net.corda.core.internal.concurrent.openFuture import net.corda.core.internal.div +import net.corda.core.internal.readText import net.corda.core.internal.times import net.corda.core.messaging.CordaRPCOps import net.corda.core.node.services.AttachmentFixup @@ -41,8 +42,10 @@ import rx.subjects.AsyncSubject import java.io.InputStream import java.net.Socket import java.net.SocketException +import java.nio.file.Path import java.sql.DriverManager import java.time.Duration +import java.time.Instant import java.util.concurrent.ScheduledExecutorService import java.util.concurrent.TimeUnit import java.util.jar.JarOutputStream @@ -79,6 +82,8 @@ val FINANCE_CORDAPPS: Set = setOf(FINANCE_CONTRACTS_CORDAPP, FI @JvmField val DUMMY_CONTRACTS_CORDAPP: CustomCordapp = cordappWithPackages("net.corda.testing.contracts") +private const val SECONDS_TO_WAIT_FOR_P2P: Long = 20 + fun cordappsForPackages(vararg packageNames: String): Set = cordappsForPackages(packageNames.asList()) fun cordappsForPackages(packageNames: Iterable): Set { @@ -172,20 +177,28 @@ fun addressMustBeBoundFuture(executorService: ScheduledExecutorService, hostAndP } fun nodeMustBeStartedFuture( - executorService: ScheduledExecutorService, - hostAndPort: NetworkHostAndPort, - listenProcess: Process? = null, - exception: () -> NodeListenProcessDeathException + executorService: ScheduledExecutorService, + logFile: Path, + listenProcess: Process, + exception: () -> NodeListenProcessDeathException ): CordaFuture { - return poll(executorService, "address $hostAndPort to bind") { - if (listenProcess != null && !listenProcess.isAlive) { + val stopPolling = Instant.now().plusSeconds(SECONDS_TO_WAIT_FOR_P2P) + return poll(executorService, "process $listenProcess is running") { + if (!listenProcess.isAlive) { throw exception() } - try { - Socket(hostAndPort.host, hostAndPort.port).close() - Unit - } catch (_exception: SocketException) { - null + when { + logFile.readText().contains("Running P2PMessaging loop") -> { + Unit + } + Instant.now().isAfter(stopPolling) -> { + // Waited for 20 seconds and the log file did not indicate that the PWP loop is running. + // This could be because the log is disabled, so lets try to create a client anyway. + Unit + } + else -> { + null + } } } } From ac4907a4297f2862143386105d82a8340f4daf5a Mon Sep 17 00:00:00 2001 From: Dan Newton Date: Tue, 14 Jul 2020 08:04:52 +0100 Subject: [PATCH 67/85] CORDA-3721 Finishing + retrying a flow cancels its future (#6454) Cancel the future being run by a flow when finishing or retrying it. The cancellation of the future no longer cares about what type of future it is. `StateMachineState` has the `future` field, which holds the 3 (currently) possible types of futures: - sleep - wait for ledger commit - async operation / external operation Move the starting of all futures triggered by actions into `ActionFutureExecutor`. --- .../node/services/statemachine/Action.kt | 8 +- .../statemachine/ActionExecutorImpl.kt | 43 ++------- .../statemachine/ActionFutureExecutor.kt | 96 +++++++++++++++++++ .../statemachine/FlowSleepScheduler.kt | 78 --------------- .../SingleThreadedStateMachineManager.kt | 32 ++++--- .../statemachine/StateMachineManager.kt | 2 - .../transitions/StartedFlowTransition.kt | 7 +- 7 files changed, 134 insertions(+), 132 deletions(-) create mode 100644 node/src/main/kotlin/net/corda/node/services/statemachine/ActionFutureExecutor.kt delete mode 100644 node/src/main/kotlin/net/corda/node/services/statemachine/FlowSleepScheduler.kt diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/Action.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/Action.kt index 51aadc69cf..6b17fe0870 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/Action.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/Action.kt @@ -17,7 +17,7 @@ sealed class Action { /** * Track a transaction hash and notify the state machine once the corresponding transaction has committed. */ - data class TrackTransaction(val hash: SecureHash) : Action() + data class TrackTransaction(val hash: SecureHash, val currentState: StateMachineState) : Action() /** * Send an initial session message to [destination]. @@ -140,7 +140,11 @@ sealed class Action { /** * Execute the specified [operation]. */ - data class ExecuteAsyncOperation(val deduplicationId: String, val operation: FlowAsyncOperation<*>) : Action() + data class ExecuteAsyncOperation( + val deduplicationId: String, + val operation: FlowAsyncOperation<*>, + val currentState: StateMachineState + ) : Action() /** * Release soft locks associated with given ID (currently the flow ID). diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/ActionExecutorImpl.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/ActionExecutorImpl.kt index 260ea86cac..2849dc03a1 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/ActionExecutorImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/ActionExecutorImpl.kt @@ -3,7 +3,6 @@ package net.corda.node.services.statemachine import co.paralleluniverse.fibers.Suspendable import com.codahale.metrics.Gauge import com.codahale.metrics.Reservoir -import net.corda.core.internal.concurrent.thenMatch import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.internal.CheckpointSerializationContext import net.corda.core.serialization.internal.checkpointSerialize @@ -15,17 +14,17 @@ import net.corda.nodeapi.internal.persistence.contextDatabase import net.corda.nodeapi.internal.persistence.contextTransaction import net.corda.nodeapi.internal.persistence.contextTransactionOrNull import java.sql.SQLException -import java.time.Duration /** * This is the bottom execution engine of flow side-effects. */ internal class ActionExecutorImpl( - private val services: ServiceHubInternal, - private val checkpointStorage: CheckpointStorage, - private val flowMessaging: FlowMessaging, - private val stateMachineManager: StateMachineManagerInternal, - private val checkpointSerializationContext: CheckpointSerializationContext + private val services: ServiceHubInternal, + private val checkpointStorage: CheckpointStorage, + private val flowMessaging: FlowMessaging, + private val stateMachineManager: StateMachineManagerInternal, + private val actionFutureExecutor: ActionFutureExecutor, + private val checkpointSerializationContext: CheckpointSerializationContext ) : ActionExecutor { private companion object { @@ -74,16 +73,8 @@ internal class ActionExecutorImpl( if (action.uuid != null) services.vaultService.softLockRelease(action.uuid) } - @Suspendable private fun executeTrackTransaction(fiber: FlowFiber, action: Action.TrackTransaction) { - services.validatedTransactions.trackTransactionWithNoWarning(action.hash).thenMatch( - success = { transaction -> - fiber.scheduleEvent(Event.TransactionCommitted(transaction)) - }, - failure = { exception -> - fiber.scheduleEvent(Event.Error(exception)) - } - ) + actionFutureExecutor.awaitTransaction(fiber, action) } @Suspendable @@ -157,13 +148,8 @@ internal class ActionExecutorImpl( fiber.scheduleEvent(action.event) } - @Suspendable private fun executeSleepUntil(fiber: FlowFiber, action: Action.SleepUntil) { - stateMachineManager.scheduleFlowSleep( - fiber, - action.currentState, - Duration.between(services.clock.instant(), action.time) - ) + actionFutureExecutor.sleep(fiber, action) } @Suspendable @@ -236,19 +222,10 @@ internal class ActionExecutorImpl( } } - @Suppress("TooGenericExceptionCaught") // this is fully intentional here, see comment in the catch clause - @Suspendable + @Suppress("TooGenericExceptionCaught") private fun executeAsyncOperation(fiber: FlowFiber, action: Action.ExecuteAsyncOperation) { try { - val operationFuture = action.operation.execute(action.deduplicationId) - operationFuture.thenMatch( - success = { result -> - fiber.scheduleEvent(Event.AsyncOperationCompletion(result)) - }, - failure = { exception -> - fiber.scheduleEvent(Event.AsyncOperationThrows(exception)) - } - ) + actionFutureExecutor.awaitAsyncOperation(fiber, action) } catch (e: Exception) { // Catch and wrap any unexpected exceptions from the async operation // Wrapping the exception allows it to be better handled by the flow hospital diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/ActionFutureExecutor.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/ActionFutureExecutor.kt new file mode 100644 index 0000000000..dc5d2fc0b9 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/ActionFutureExecutor.kt @@ -0,0 +1,96 @@ +package net.corda.node.services.statemachine + +import net.corda.core.internal.concurrent.thenMatch +import net.corda.core.utilities.contextLogger +import net.corda.core.utilities.debug +import net.corda.node.services.api.ServiceHubInternal +import java.time.Duration +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.TimeUnit + +internal class ActionFutureExecutor( + private val innerState: StateMachineInnerState, + private val services: ServiceHubInternal, + private val scheduledExecutor: ScheduledExecutorService +) { + + private companion object { + val log = contextLogger() + } + + /** + * Put a flow to sleep for the duration specified in [action]. + * + * @param fiber The [FlowFiber] that will be woken up after sleeping + * @param action The [Action.SleepUntil] to create a future from + */ + fun sleep(fiber: FlowFiber, action: Action.SleepUntil) { + cancelFutureIfRunning(fiber, action.currentState) + val instance = fiber.instanceId + val duration = Duration.between(services.clock.instant(), action.time) + log.debug { "Putting flow ${instance.runId} to sleep for $duration" } + val future = scheduledExecutor.schedule( + { + log.debug { "Scheduling flow wake up event for flow ${instance.runId}" } + scheduleWakeUpEvent(instance, Event.WakeUpFromSleep) + }, + duration.toMillis(), TimeUnit.MILLISECONDS + ) + action.currentState.future = future + } + + /** + * Suspend a flow until its async operation specified in [action] is completed. + * + * @param fiber The [FlowFiber] to resume after completing the async operation + * @param action The [Action.ExecuteAsyncOperation] to create a future from + */ + fun awaitAsyncOperation(fiber: FlowFiber, action: Action.ExecuteAsyncOperation) { + cancelFutureIfRunning(fiber, action.currentState) + val instance = fiber.instanceId + log.debug { "Suspending flow ${instance.runId} until its async operation has completed" } + val future = action.operation.execute(action.deduplicationId) + future.thenMatch( + success = { result -> scheduleWakeUpEvent(instance, Event.AsyncOperationCompletion(result)) }, + failure = { exception -> scheduleWakeUpEvent(instance, Event.AsyncOperationThrows(exception)) } + ) + action.currentState.future = future + } + + /** + * Suspend a flow until the transaction specified in [action] is committed. + * + * @param fiber The [FlowFiber] to resume after the committing the specified transaction + * @param action [Action.TrackTransaction] contains the transaction hash to wait for + */ + fun awaitTransaction(fiber: FlowFiber, action: Action.TrackTransaction) { + cancelFutureIfRunning(fiber, action.currentState) + val instance = fiber.instanceId + log.debug { "Suspending flow ${instance.runId} until transaction ${action.hash} is committed" } + val future = services.validatedTransactions.trackTransactionWithNoWarning(action.hash) + future.thenMatch( + success = { transaction -> scheduleWakeUpEvent(instance, Event.TransactionCommitted(transaction)) }, + failure = { exception -> scheduleWakeUpEvent(instance, Event.Error(exception)) } + ) + action.currentState.future = future + } + + private fun cancelFutureIfRunning(fiber: FlowFiber, currentState: StateMachineState) { + // No other future should be running, cancel it if there is + currentState.future?.run { + log.debug { "Cancelling existing future for flow ${fiber.id}" } + if (!isDone) cancel(true) + } + } + + private fun scheduleWakeUpEvent(instance: StateMachineInstanceId, event: Event) { + innerState.withLock { + flows[instance.runId]?.let { flow -> + // Only schedule a wake up event if the fiber the flow is executing on has not changed + if (flow.fiber.instanceId == instance) { + flow.fiber.scheduleEvent(event) + } + } + } + } +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowSleepScheduler.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowSleepScheduler.kt deleted file mode 100644 index 63fcd5c6e8..0000000000 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowSleepScheduler.kt +++ /dev/null @@ -1,78 +0,0 @@ -package net.corda.node.services.statemachine - -import net.corda.core.internal.FlowIORequest -import net.corda.core.utilities.contextLogger -import net.corda.core.utilities.debug -import java.time.Duration -import java.util.concurrent.Future -import java.util.concurrent.ScheduledExecutorService -import java.util.concurrent.ScheduledFuture -import java.util.concurrent.TimeUnit - -internal class FlowSleepScheduler(private val innerState: StateMachineInnerState, private val scheduledExecutor: ScheduledExecutorService) { - - private companion object { - val log = contextLogger() - } - - /** - * Put a flow to sleep for a specified duration. - * - * @param fiber The [FlowFiber] that will be woken up after sleeping - * @param currentState The current [StateMachineState] - * @param duration How long to sleep for - */ - fun sleep(fiber: FlowFiber, currentState: StateMachineState, duration: Duration) { - // No other future should be running, cancel it if there is - currentState.future?.run { - log.debug { "Cancelling the existing future for flow ${fiber.id}" } - cancelIfRunning() - } - currentState.future = setAlarmClock(fiber, duration) - } - - /** - * Cancel a sleeping flow's future. Note, this does not cause the flow to wake up. - * - * @param currentState The current [StateMachineState] - */ - fun cancel(currentState: StateMachineState) { - (currentState.checkpoint.flowState as? FlowState.Started)?.let { flowState -> - if (currentState.isWaitingForFuture && flowState.flowIORequest is FlowIORequest.Sleep) { - (currentState.future as? ScheduledFuture)?.run { - log.debug { "Cancelling the sleep scheduled future for flow ${currentState.flowLogic.runId}" } - cancelIfRunning() - currentState.future = null - } - } - - } - } - - private fun Future<*>.cancelIfRunning() { - if (!isDone) cancel(true) - } - - private fun setAlarmClock(fiber: FlowFiber, duration: Duration): ScheduledFuture { - val instance = fiber.instanceId - log.debug { "Putting flow ${instance.runId} to sleep for $duration" } - return scheduledExecutor.schedule( - { - log.debug { "Scheduling flow wake up event for flow ${instance.runId}" } - scheduleWakeUp(instance) - }, - duration.toMillis(), TimeUnit.MILLISECONDS - ) - } - - private fun scheduleWakeUp(instance: StateMachineInstanceId) { - innerState.withLock { - flows[instance.runId]?.let { flow -> - // Only schedule a wake up event if the fiber the flow is executing on has not changed - if (flow.fiber.instanceId == instance) { - flow.fiber.scheduleEvent(Event.WakeUpFromSleep) - } - } - } - } -} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/SingleThreadedStateMachineManager.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/SingleThreadedStateMachineManager.kt index 2914aecd5d..1d07a75d02 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/SingleThreadedStateMachineManager.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/SingleThreadedStateMachineManager.kt @@ -47,9 +47,8 @@ import net.corda.serialization.internal.withTokenContext import org.apache.activemq.artemis.utils.ReusableLatch import rx.Observable import java.security.SecureRandom -import java.time.Duration +import java.util.ArrayList import java.util.HashSet -import java.util.* import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ExecutorService import java.util.concurrent.Executors @@ -88,7 +87,7 @@ internal class SingleThreadedStateMachineManager( private val metrics = serviceHub.monitoringService.metrics private val sessionToFlow = ConcurrentHashMap() private val flowMessaging: FlowMessaging = FlowMessagingImpl(serviceHub) - private val flowSleepScheduler = FlowSleepScheduler(innerState, scheduledFutureExecutor) + private val actionFutureExecutor = ActionFutureExecutor(innerState, serviceHub, scheduledFutureExecutor) private val flowTimeoutScheduler = FlowTimeoutScheduler(innerState, scheduledFutureExecutor, serviceHub) private val fiberDeserializationChecker = if (serviceHub.configuration.shouldCheckCheckpoints()) FiberDeserializationChecker() else null private val ourSenderUUID = serviceHub.networkService.ourSenderUUID @@ -316,7 +315,7 @@ internal class SingleThreadedStateMachineManager( override fun removeFlow(flowId: StateMachineRunId, removalReason: FlowRemovalReason, lastState: StateMachineState) { innerState.withLock { flowTimeoutScheduler.cancel(flowId) - flowSleepScheduler.cancel(lastState) + lastState.cancelFutureIfRunning() val flow = flows.remove(flowId) if (flow != null) { decrementLiveFibers() @@ -384,7 +383,7 @@ internal class SingleThreadedStateMachineManager( @Suppress("TooGenericExceptionCaught", "ComplexMethod", "MaxLineLength") // this is fully intentional here, see comment in the catch clause override fun retryFlowFromSafePoint(currentState: StateMachineState) { - flowSleepScheduler.cancel(currentState) + currentState.cancelFutureIfRunning() // Get set of external events val flowId = currentState.flowLogic.runId val oldFlowLeftOver = innerState.withLock { flows[flowId] }?.fiber?.transientValues?.value?.eventQueue @@ -634,10 +633,6 @@ internal class SingleThreadedStateMachineManager( flowTimeoutScheduler.cancel(flowId) } - override fun scheduleFlowSleep(fiber: FlowFiber, currentState: StateMachineState, duration: Duration) { - flowSleepScheduler.sleep(fiber, currentState, duration) - } - private fun tryDeserializeCheckpoint(serializedCheckpoint: Checkpoint.Serialized, flowId: StateMachineRunId): Checkpoint? { return try { serializedCheckpoint.deserialize(checkpointSerializationContext!!) @@ -695,11 +690,12 @@ internal class SingleThreadedStateMachineManager( private fun makeActionExecutor(checkpointSerializationContext: CheckpointSerializationContext): ActionExecutor { return ActionExecutorImpl( - serviceHub, - checkpointStorage, - flowMessaging, - this, - checkpointSerializationContext + serviceHub, + checkpointStorage, + flowMessaging, + this, + actionFutureExecutor, + checkpointSerializationContext ) } @@ -781,4 +777,12 @@ internal class SingleThreadedStateMachineManager( } } } + + private fun StateMachineState.cancelFutureIfRunning() { + future?.run { + logger.debug { "Cancelling future for flow ${flowLogic.runId}" } + if (!isDone) cancel(true) + future = null + } + } } diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt index 66a5a60797..6c5050962b 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt @@ -10,7 +10,6 @@ import net.corda.core.utilities.Try import net.corda.node.services.messaging.DeduplicationHandler import net.corda.node.services.messaging.ReceivedMessage import rx.Observable -import java.time.Duration /** * A StateMachineManager is responsible for coordination and persistence of multiple [FlowStateMachine] objects. @@ -110,7 +109,6 @@ internal interface StateMachineManagerInternal { fun retryFlowFromSafePoint(currentState: StateMachineState) fun scheduleFlowTimeout(flowId: StateMachineRunId) fun cancelFlowTimeout(flowId: StateMachineRunId) - fun scheduleFlowSleep(fiber: FlowFiber, currentState: StateMachineState, duration: Duration) } /** diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/transitions/StartedFlowTransition.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/transitions/StartedFlowTransition.kt index 904ab3f06a..96b6557829 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/transitions/StartedFlowTransition.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/transitions/StartedFlowTransition.kt @@ -105,11 +105,12 @@ class StartedFlowTransition( // This ensures that the [WaitForLedgerCommit] request is not executed multiple times if extra // [DoRemainingWork] events are pushed onto the fiber's event queue before the flow has really woken up return if (!startingState.isWaitingForFuture) { + val state = startingState.copy(isWaitingForFuture = true) TransitionResult( - newState = startingState.copy(isWaitingForFuture = true), + newState = state, actions = listOf( Action.CreateTransaction, - Action.TrackTransaction(flowIORequest.hash), + Action.TrackTransaction(flowIORequest.hash, state), Action.CommitTransaction ) ) @@ -432,8 +433,8 @@ class StartedFlowTransition( // The `numberOfSuspends` is added to the deduplication ID in case an async // operation is executed multiple times within the same flow. val deduplicationId = context.id.toString() + ":" + currentState.checkpoint.checkpointState.numberOfSuspends.toString() - actions.add(Action.ExecuteAsyncOperation(deduplicationId, flowIORequest.operation)) currentState = currentState.copy(isWaitingForFuture = true) + actions += Action.ExecuteAsyncOperation(deduplicationId, flowIORequest.operation, currentState) FlowContinuation.ProcessEvents } } else { From 75bade2f9292c5b5cf85466ef2d85a2703fceb1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Waldemar=20=C5=BBurowski?= <45210402+wzur-r3@users.noreply.github.com> Date: Wed, 15 Jul 2020 07:46:26 +0100 Subject: [PATCH 68/85] Added NexusIQ scan to nightly snapshot build (#6461) * also build on standard agent, no K8S requirements --- .ci/dev/publish-branch/Jenkinsfile.nightly | 37 +++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/.ci/dev/publish-branch/Jenkinsfile.nightly b/.ci/dev/publish-branch/Jenkinsfile.nightly index f7a35981f6..14b2452af8 100644 --- a/.ci/dev/publish-branch/Jenkinsfile.nightly +++ b/.ci/dev/publish-branch/Jenkinsfile.nightly @@ -1,11 +1,27 @@ #!groovy +/** + * Jenkins pipeline to build Corda OS nightly snapshots + */ + +/** + * Kill already started job. + * Assume new commit takes precendence and results from previous + * unfinished builds are not required. + * This feature doesn't play well with disableConcurrentBuilds() option + */ @Library('corda-shared-build-pipeline-steps') import static com.r3.build.BuildControl.killAllExistingBuildsForJob killAllExistingBuildsForJob(env.JOB_NAME, env.BUILD_NUMBER.toInteger()) +/* +** calculate the stage for NexusIQ evaluation +** * build for snapshots +*/ +def nexusIqStage = "build" + pipeline { - agent { label 'k8s' } + agent { label 'standard' } options { timestamps() @@ -27,6 +43,25 @@ pipeline { } stages { + stage('Sonatype Check') { + steps { + sh "./gradlew --no-daemon clean jar" + script { + sh "./gradlew --no-daemon properties | grep -E '^(version|group):' >version-properties" + def version = sh (returnStdout: true, script: "grep ^version: version-properties | sed -e 's/^version: //'").trim() + def groupId = sh (returnStdout: true, script: "grep ^group: version-properties | sed -e 's/^group: //'").trim() + def artifactId = 'corda' + nexusAppId = "jenkins-${groupId}-${artifactId}-${version}" + } + nexusPolicyEvaluation ( + failBuildOnNetworkError: false, + iqApplication: manualApplication(nexusAppId), + iqScanPatterns: [[scanPattern: 'node/capsule/build/libs/corda*.jar']], + iqStage: nexusIqStage + ) + } + } + stage('Publish to Artifactory') { steps { rtServer ( From 57e5f27961686b84d780e030626463f554662861 Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Wed, 15 Jul 2020 09:09:28 +0100 Subject: [PATCH 69/85] CORDA-3906: Allow usage of SchedulableState in deterministic CorDapps. (#6457) --- .../src/main/kotlin/net/corda/core/contracts/Structures.kt | 6 ++++-- core/src/main/kotlin/net/corda/core/flows/FlowLogicRef.kt | 7 +++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/contracts/Structures.kt b/core/src/main/kotlin/net/corda/core/contracts/Structures.kt index 16d2c8cd85..bfae2f8900 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/Structures.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/Structures.kt @@ -89,6 +89,7 @@ interface OwnableState : ContractState { // DOCEND 3 /** Something which is scheduled to happen at a point in time. */ +@KeepForDJVM interface Scheduled { val scheduledAt: Instant } @@ -101,6 +102,7 @@ interface Scheduled { * lifecycle processing needs to take place. e.g. a fixing or a late payment etc. */ @CordaSerializable +@KeepForDJVM data class ScheduledStateRef(val ref: StateRef, override val scheduledAt: Instant) : Scheduled /** @@ -115,7 +117,7 @@ data class ScheduledStateRef(val ref: StateRef, override val scheduledAt: Instan * for a particular [ContractState] have been processed/fired etc. If the activity is not "on ledger" then the * scheduled activity shouldn't be either. */ -@DeleteForDJVM +@KeepForDJVM data class ScheduledActivity(val logicRef: FlowLogicRef, override val scheduledAt: Instant) : Scheduled // DOCSTART 2 @@ -134,7 +136,7 @@ interface LinearState : ContractState { val linearId: UniqueIdentifier } // DOCEND 2 -@DeleteForDJVM +@KeepForDJVM interface SchedulableState : ContractState { /** * Indicate whether there is some activity to be performed at some future point in time with respect to this diff --git a/core/src/main/kotlin/net/corda/core/flows/FlowLogicRef.kt b/core/src/main/kotlin/net/corda/core/flows/FlowLogicRef.kt index 7781c38b95..1b08620e2a 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FlowLogicRef.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FlowLogicRef.kt @@ -1,7 +1,9 @@ package net.corda.core.flows import net.corda.core.CordaInternal +import net.corda.core.DeleteForDJVM import net.corda.core.DoNotImplement +import net.corda.core.KeepForDJVM import net.corda.core.serialization.CordaSerializable /** @@ -11,11 +13,13 @@ import net.corda.core.serialization.CordaSerializable * the flow to run at the scheduled time. */ @DoNotImplement +@KeepForDJVM interface FlowLogicRefFactory { /** * Construct a FlowLogicRef. This is intended for cases where the calling code has the relevant class already * and can provide it directly. */ + @DeleteForDJVM fun create(flowClass: Class>, vararg args: Any?): FlowLogicRef /** @@ -30,12 +34,14 @@ interface FlowLogicRefFactory { * [SchedulableFlow] annotation. */ @CordaInternal + @DeleteForDJVM fun createForRPC(flowClass: Class>, vararg args: Any?): FlowLogicRef /** * Converts a [FlowLogicRef] object that was obtained from the calls above into a [FlowLogic], after doing some * validation to ensure it points to a legitimate flow class. */ + @DeleteForDJVM fun toFlowLogic(ref: FlowLogicRef): FlowLogic<*> } @@ -59,4 +65,5 @@ class IllegalFlowLogicException(val type: String, msg: String) : // TODO: align this with the existing [FlowRef] in the bank-side API (probably replace some of the API classes) @CordaSerializable @DoNotImplement +@KeepForDJVM interface FlowLogicRef \ No newline at end of file From ef9934ed4e18dbdbc30ad57425be8d482e80fff6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Waldemar=20=C5=BBurowski?= <45210402+wzur-r3@users.noreply.github.com> Date: Wed, 15 Jul 2020 10:17:58 +0100 Subject: [PATCH 70/85] Added build discarder settings back (#6465) builds are expired only after 14 days --- .ci/dev/compatibility/JenkinsfileJDK11Compile | 3 ++- .ci/dev/mswin/Jenkinsfile | 1 + .ci/dev/nightly-regression/Jenkinsfile | 1 + .ci/dev/pr-code-checks/Jenkinsfile | 1 + .ci/dev/publish-branch/Jenkinsfile.nightly | 1 + .ci/dev/publish-branch/Jenkinsfile.preview | 1 + .ci/dev/regression/Jenkinsfile | 1 + Jenkinsfile | 1 + 8 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.ci/dev/compatibility/JenkinsfileJDK11Compile b/.ci/dev/compatibility/JenkinsfileJDK11Compile index d2251fad15..f6e9c43195 100644 --- a/.ci/dev/compatibility/JenkinsfileJDK11Compile +++ b/.ci/dev/compatibility/JenkinsfileJDK11Compile @@ -14,6 +14,7 @@ pipeline { options { timestamps() timeout(time: 3, unit: 'HOURS') + buildDiscarder(logRotator(daysToKeepStr: '14', artifactDaysToKeepStr: '14')) } stages { @@ -35,4 +36,4 @@ pipeline { deleteDir() /* clean up our workspace */ } } -} \ No newline at end of file +} diff --git a/.ci/dev/mswin/Jenkinsfile b/.ci/dev/mswin/Jenkinsfile index 923a83ff85..b0e3766e0c 100644 --- a/.ci/dev/mswin/Jenkinsfile +++ b/.ci/dev/mswin/Jenkinsfile @@ -28,6 +28,7 @@ pipeline { ansiColor('xterm') timestamps() timeout(time: 3, unit: 'HOURS') + buildDiscarder(logRotator(daysToKeepStr: '14', artifactDaysToKeepStr: '14')) /* * a bit awkward to read diff --git a/.ci/dev/nightly-regression/Jenkinsfile b/.ci/dev/nightly-regression/Jenkinsfile index dc3979ae23..5c2af45b99 100644 --- a/.ci/dev/nightly-regression/Jenkinsfile +++ b/.ci/dev/nightly-regression/Jenkinsfile @@ -9,6 +9,7 @@ pipeline { timestamps() overrideIndexTriggers(false) timeout(time: 3, unit: 'HOURS') + buildDiscarder(logRotator(daysToKeepStr: '14', artifactDaysToKeepStr: '14')) } triggers { pollSCM ignorePostCommitHooks: true, scmpoll_spec: '@midnight' diff --git a/.ci/dev/pr-code-checks/Jenkinsfile b/.ci/dev/pr-code-checks/Jenkinsfile index c74639b4f6..a64813c92f 100644 --- a/.ci/dev/pr-code-checks/Jenkinsfile +++ b/.ci/dev/pr-code-checks/Jenkinsfile @@ -8,6 +8,7 @@ pipeline { options { timestamps() timeout(time: 3, unit: 'HOURS') + buildDiscarder(logRotator(daysToKeepStr: '14', artifactDaysToKeepStr: '14')) } environment { diff --git a/.ci/dev/publish-branch/Jenkinsfile.nightly b/.ci/dev/publish-branch/Jenkinsfile.nightly index f7a35981f6..b25549729e 100644 --- a/.ci/dev/publish-branch/Jenkinsfile.nightly +++ b/.ci/dev/publish-branch/Jenkinsfile.nightly @@ -12,6 +12,7 @@ pipeline { ansiColor('xterm') overrideIndexTriggers(false) timeout(time: 3, unit: 'HOURS') + buildDiscarder(logRotator(daysToKeepStr: '14', artifactDaysToKeepStr: '14')) } triggers { diff --git a/.ci/dev/publish-branch/Jenkinsfile.preview b/.ci/dev/publish-branch/Jenkinsfile.preview index e0fb92aa77..e66deeabab 100644 --- a/.ci/dev/publish-branch/Jenkinsfile.preview +++ b/.ci/dev/publish-branch/Jenkinsfile.preview @@ -12,6 +12,7 @@ pipeline { ansiColor('xterm') overrideIndexTriggers(false) timeout(time: 3, unit: 'HOURS') + buildDiscarder(logRotator(daysToKeepStr: '14', artifactDaysToKeepStr: '14')) } environment { diff --git a/.ci/dev/regression/Jenkinsfile b/.ci/dev/regression/Jenkinsfile index 51f507dac2..4ad11d28f4 100644 --- a/.ci/dev/regression/Jenkinsfile +++ b/.ci/dev/regression/Jenkinsfile @@ -38,6 +38,7 @@ pipeline { options { timestamps() timeout(time: 3, unit: 'HOURS') + buildDiscarder(logRotator(daysToKeepStr: '14', artifactDaysToKeepStr: '14')) } environment { diff --git a/Jenkinsfile b/Jenkinsfile index 02365f00a4..5c9a1add5d 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -9,6 +9,7 @@ pipeline { options { timestamps() timeout(time: 3, unit: 'HOURS') + buildDiscarder(logRotator(daysToKeepStr: '14', artifactDaysToKeepStr: '14')) } environment { From 6bae99ef1009b9a64218396d458af9c5c22a4058 Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Wed, 15 Jul 2020 10:46:29 +0100 Subject: [PATCH 71/85] CORDA-3909: Upgrade to Corda Gradle plugins 5.0.11. (#6463) This also upgrades the following plugins: - Artifactory: 4.7.3 -> 4.16.1 - Bintray: 1.4 -> 1.8.5 --- build.gradle | 4 +--- constants.properties | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index 5286b90831..039ce0ebc8 100644 --- a/build.gradle +++ b/build.gradle @@ -170,7 +170,6 @@ buildscript { dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-allopen:$kotlin_version" - classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.4' classpath "net.corda.plugins:publish-utils:$gradle_plugins_version" classpath "net.corda.plugins:quasar-utils:$gradle_plugins_version" classpath "net.corda.plugins:cordformation:$gradle_plugins_version" @@ -204,7 +203,6 @@ plugins { apply plugin: 'project-report' apply plugin: 'com.github.ben-manes.versions' apply plugin: 'net.corda.plugins.publish-utils' -apply plugin: 'maven-publish' apply plugin: 'com.jfrog.artifactory' apply plugin: "com.bmuschko.docker-remote-api" apply plugin: "com.r3.dependx.dependxies" @@ -626,7 +624,7 @@ dependxiesModule { skipTasks = "test,integrationTest,smokeTest,slowIntegrationTest" } -tasks.register('generateApi', net.corda.plugins.GenerateApi) { +tasks.register('generateApi', net.corda.plugins.apiscanner.GenerateApi) { baseName = "api-corda" } diff --git a/constants.properties b/constants.properties index 2b2775389d..c9877793fb 100644 --- a/constants.properties +++ b/constants.properties @@ -4,7 +4,7 @@ cordaVersion=4.6 versionSuffix=SNAPSHOT -gradlePluginsVersion=5.0.9 +gradlePluginsVersion=5.0.11 kotlinVersion=1.2.71 java8MinUpdateVersion=171 # ***************************************************************# @@ -25,7 +25,7 @@ classgraphVersion=4.8.78 disruptorVersion=3.4.2 typesafeConfigVersion=1.3.4 jsr305Version=3.0.2 -artifactoryPluginVersion=4.7.3 +artifactoryPluginVersion=4.16.1 snakeYamlVersion=1.19 caffeineVersion=2.7.0 metricsVersion=4.1.0 From b58609daba10b3be44b7f0cce48ecd831a2f9d75 Mon Sep 17 00:00:00 2001 From: Waldemar Zurowski Date: Wed, 15 Jul 2020 21:33:49 +0100 Subject: [PATCH 72/85] Correct name of Artifactory repository for publishing releases --- .ci/dev/regression/Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/dev/regression/Jenkinsfile b/.ci/dev/regression/Jenkinsfile index 4ad11d28f4..0361303e5b 100644 --- a/.ci/dev/regression/Jenkinsfile +++ b/.ci/dev/regression/Jenkinsfile @@ -134,7 +134,7 @@ pipeline { rtGradleDeployer( id: 'deployer', serverId: 'R3-Artifactory', - repo: 'r3-corda-releases' + repo: 'corda-releases' ) rtGradleRun( usesPlugin: true, From 2fa6b5a208c432ef209c208daadba6b4a1a46286 Mon Sep 17 00:00:00 2001 From: Adel El-Beik <48713346+adelel1@users.noreply.github.com> Date: Thu, 16 Jul 2020 09:58:36 +0100 Subject: [PATCH 73/85] CORDA-3769: Switched attachments class loader cache to use caffeine (#6326) * CORDA-3769: Switched attachments class loader cache to use caffeine with original implementation used by determinstic core. * CORDA-3769: Removed default ctor arguments. * CORDA-3769: Switched mapping function to Function type to avoid synthetic method being generated. * CORDA-3769: Now using a cache created from NamedCacheFactory for the attachments class loader cache. * CORDA-3769: Making detekt happy. * CORDA-3769: The finality tests now check for UntrustedAttachmentsException which will actually happen in reality. * CORDA-3769: Refactored after review comments. * CORDA-3769: Removed the AttachmentsClassLoaderSimpleCacheImpl as DJVM does not need it. Also updated due to review comments. * CORDA-3769: Removed the generic parameters from AttachmentsClassLoader. * CORDA-3769: Removed unused imports. * CORDA-3769: Updates from review comments. * CORDA-3769: Updated following review comments. MigrationServicesForResolution now uses cache factory. Ctor updated for AttachmentsClassLoaderSimpleCacheImpl. * CORDA-3769: Reduced max class loader cache size * CORDA-3769: Fixed the attachments class loader cache size to a fixed default * CORDA-3769: Switched attachments class loader size to be reduced by fixed value. --- .../flows/ReceiveFinalityFlowTest.kt | 6 +-- ...ttachmentsClassLoaderSerializationTests.kt | 2 +- .../AttachmentsClassLoaderTests.kt | 5 +- .../transactions/TransactionTests.kt | 10 +++- .../core/internal/ServiceHubCoreInternal.kt | 3 ++ .../internal/AttachmentsClassLoader.kt | 52 +++++++++++++++---- .../ContractUpgradeTransactions.kt | 3 +- .../core/transactions/LedgerTransaction.kt | 34 ++++++++---- .../core/transactions/WireTransaction.kt | 17 ++++-- .../internal/internalAccessTestHelpers.kt | 7 ++- .../net/corda/node/internal/AbstractNode.kt | 5 ++ .../migration/MigrationNamedCacheFactory.kt | 1 + .../MigrationServicesForResolution.kt | 7 ++- .../corda/node/utilities/NodeNamedCache.kt | 5 +- .../node/services/FinalityHandlerTest.kt | 8 +-- .../kotlin/net/corda/testing/dsl/TestDSL.kt | 4 ++ .../internal/TestingNamedCacheFactory.kt | 1 + 17 files changed, 127 insertions(+), 43 deletions(-) diff --git a/core-tests/src/test/kotlin/net/corda/coretests/flows/ReceiveFinalityFlowTest.kt b/core-tests/src/test/kotlin/net/corda/coretests/flows/ReceiveFinalityFlowTest.kt index 6c50e4e2a2..2a45e2105e 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/flows/ReceiveFinalityFlowTest.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/flows/ReceiveFinalityFlowTest.kt @@ -53,7 +53,7 @@ class ReceiveFinalityFlowTest { val paymentReceiverId = paymentReceiverFuture.getOrThrow() assertThat(bob.services.vaultService.queryBy>().states).isEmpty() - bob.assertFlowSentForObservationDueToConstraintError(paymentReceiverId) + 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))) @@ -69,7 +69,7 @@ class ReceiveFinalityFlowTest { .ofType(R::class.java) } - private fun TestStartedNode.assertFlowSentForObservationDueToConstraintError(runId: StateMachineRunId) { + private fun TestStartedNode.assertFlowSentForObservationDueToUntrustedAttachmentsException(runId: StateMachineRunId) { val observation = medicalRecordsOfType() .filter { it.flowId == runId } .toBlocking() @@ -77,6 +77,6 @@ class ReceiveFinalityFlowTest { assertThat(observation.outcome).isEqualTo(Outcome.OVERNIGHT_OBSERVATION) assertThat(observation.by).contains(FinalityDoctor) val error = observation.errors.single() - assertThat(error).isInstanceOf(TransactionVerificationException.ContractConstraintRejection::class.java) + assertThat(error).isInstanceOf(TransactionVerificationException.UntrustedAttachmentsException::class.java) } } diff --git a/core-tests/src/test/kotlin/net/corda/coretests/transactions/AttachmentsClassLoaderSerializationTests.kt b/core-tests/src/test/kotlin/net/corda/coretests/transactions/AttachmentsClassLoaderSerializationTests.kt index 5cfaf252cb..4ca58d6b46 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/transactions/AttachmentsClassLoaderSerializationTests.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/transactions/AttachmentsClassLoaderSerializationTests.kt @@ -55,7 +55,7 @@ class AttachmentsClassLoaderSerializationTests { arrayOf(isolatedId, att1, att2).map { storage.openAttachment(it)!! }, testNetworkParameters(), SecureHash.zeroHash, - { attachmentTrustCalculator.calculate(it) }) { classLoader -> + { attachmentTrustCalculator.calculate(it) }, attachmentsClassLoaderCache = null) { classLoader -> val contractClass = Class.forName(ISOLATED_CONTRACT_CLASS_NAME, true, classLoader) val contract = contractClass.getDeclaredConstructor().newInstance() as Contract assertEquals("helloworld", contract.declaredField("magicString").value) diff --git a/core-tests/src/test/kotlin/net/corda/coretests/transactions/AttachmentsClassLoaderTests.kt b/core-tests/src/test/kotlin/net/corda/coretests/transactions/AttachmentsClassLoaderTests.kt index fcc081efb6..986b6052ef 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/transactions/AttachmentsClassLoaderTests.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/transactions/AttachmentsClassLoaderTests.kt @@ -23,6 +23,7 @@ import net.corda.core.internal.inputStream import net.corda.core.node.NetworkParameters import net.corda.core.node.services.AttachmentId import net.corda.core.serialization.internal.AttachmentsClassLoader +import net.corda.core.serialization.internal.AttachmentsClassLoaderCacheImpl import net.corda.testing.common.internal.testNetworkParameters import net.corda.node.services.attachments.NodeAttachmentTrustCalculator import net.corda.testing.contracts.DummyContract @@ -521,6 +522,7 @@ class AttachmentsClassLoaderTests { val id = SecureHash.randomSHA256() val timeWindow: TimeWindow? = null val privacySalt = PrivacySalt() + val attachmentsClassLoaderCache = AttachmentsClassLoaderCacheImpl(cacheFactory) val transaction = createLedgerTransaction( inputs, outputs, @@ -532,7 +534,8 @@ class AttachmentsClassLoaderTests { privacySalt, testNetworkParameters(), emptyList(), - isAttachmentTrusted = { true } + isAttachmentTrusted = { true }, + attachmentsClassLoaderCache = attachmentsClassLoaderCache ) transaction.verify() } diff --git a/core-tests/src/test/kotlin/net/corda/coretests/transactions/TransactionTests.kt b/core-tests/src/test/kotlin/net/corda/coretests/transactions/TransactionTests.kt index ec14b84029..500cfa9816 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/transactions/TransactionTests.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/transactions/TransactionTests.kt @@ -10,6 +10,7 @@ import net.corda.core.internal.AbstractAttachment import net.corda.core.internal.TESTDSL_UPLOADER import net.corda.core.internal.createLedgerTransaction import net.corda.core.node.NotaryInfo +import net.corda.core.serialization.internal.AttachmentsClassLoaderCacheImpl import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.WireTransaction import net.corda.testing.common.internal.testNetworkParameters @@ -18,6 +19,7 @@ import net.corda.testing.core.* import net.corda.testing.internal.createWireTransaction import net.corda.testing.internal.fakeAttachment import net.corda.coretesting.internal.rigorousMock +import net.corda.testing.internal.TestingNamedCacheFactory import org.junit.Rule import org.junit.Test import java.math.BigInteger @@ -131,6 +133,7 @@ class TransactionTests { val id = SecureHash.randomSHA256() val timeWindow: TimeWindow? = null val privacySalt = PrivacySalt() + val attachmentsClassLoaderCache = AttachmentsClassLoaderCacheImpl(TestingNamedCacheFactory()) val transaction = createLedgerTransaction( inputs, outputs, @@ -142,7 +145,8 @@ class TransactionTests { privacySalt, testNetworkParameters(), emptyList(), - isAttachmentTrusted = { true } + isAttachmentTrusted = { true }, + attachmentsClassLoaderCache = attachmentsClassLoaderCache ) transaction.verify() @@ -183,6 +187,7 @@ class TransactionTests { val id = SecureHash.randomSHA256() val timeWindow: TimeWindow? = null val privacySalt = PrivacySalt() + val attachmentsClassLoaderCache = AttachmentsClassLoaderCacheImpl(TestingNamedCacheFactory()) fun buildTransaction() = createLedgerTransaction( inputs, @@ -195,7 +200,8 @@ class TransactionTests { privacySalt, testNetworkParameters(notaries = listOf(NotaryInfo(DUMMY_NOTARY, true))), emptyList(), - isAttachmentTrusted = { true } + isAttachmentTrusted = { true }, + attachmentsClassLoaderCache = attachmentsClassLoaderCache ) assertFailsWith { buildTransaction().verify() } diff --git a/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt b/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt index 5897984c1d..2e59429fb5 100644 --- a/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt +++ b/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt @@ -5,6 +5,7 @@ import net.corda.core.DeleteForDJVM import net.corda.core.internal.notary.NotaryService import net.corda.core.node.ServiceHub import net.corda.core.node.StatesToRecord +import net.corda.core.serialization.internal.AttachmentsClassLoaderCache import java.util.concurrent.ExecutorService // TODO: This should really be called ServiceHubInternal but that name is already taken by net.corda.node.services.api.ServiceHubInternal. @@ -21,6 +22,8 @@ interface ServiceHubCoreInternal : ServiceHub { val notaryService: NotaryService? fun createTransactionsResolver(flow: ResolveTransactionsFlow): TransactionsResolver + + val attachmentsClassLoaderCache: AttachmentsClassLoaderCache } interface TransactionsResolver { diff --git a/core/src/main/kotlin/net/corda/core/serialization/internal/AttachmentsClassLoader.kt b/core/src/main/kotlin/net/corda/core/serialization/internal/AttachmentsClassLoader.kt index eed759a08e..e93be2de5d 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/internal/AttachmentsClassLoader.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/internal/AttachmentsClassLoader.kt @@ -1,5 +1,8 @@ package net.corda.core.serialization.internal +import com.github.benmanes.caffeine.cache.Cache +import com.github.benmanes.caffeine.cache.Caffeine +import net.corda.core.DeleteForDJVM import net.corda.core.contracts.Attachment import net.corda.core.contracts.ContractAttachment import net.corda.core.contracts.TransactionVerificationException @@ -21,6 +24,7 @@ import java.lang.ref.WeakReference import java.net.* import java.security.Permission import java.util.* +import java.util.function.Function /** * A custom ClassLoader that knows how to load classes from a set of attachments. The attachments themselves only @@ -289,31 +293,27 @@ class AttachmentsClassLoader(attachments: List, */ @VisibleForTesting object AttachmentsClassLoaderBuilder { - private const val CACHE_SIZE = 1000 + const val CACHE_SIZE = 16 - // We use a set here because the ordering of attachments doesn't affect code execution, due to the no - // overlap rule, and attachments don't have any particular ordering enforced by the builders. So we - // can just do unordered comparisons here. But the same attachments run with different network parameters - // may behave differently, so that has to be a part of the cache key. - private data class Key(val hashes: Set, val params: NetworkParameters) - - // This runs in the DJVM so it can't use caffeine. - private val cache: MutableMap = createSimpleCache(CACHE_SIZE).toSynchronised() + private val fallBackCache: AttachmentsClassLoaderCache = AttachmentsClassLoaderSimpleCacheImpl(CACHE_SIZE) /** * Runs the given block with serialization execution context set up with a (possibly cached) attachments classloader. * * @param txId The transaction ID that triggered this request; it's unused except for error messages and exceptions that can occur during setup. */ + @Suppress("LongParameterList") fun withAttachmentsClassloaderContext(attachments: List, params: NetworkParameters, txId: SecureHash, isAttachmentTrusted: (Attachment) -> Boolean, parent: ClassLoader = ClassLoader.getSystemClassLoader(), + attachmentsClassLoaderCache: AttachmentsClassLoaderCache?, block: (ClassLoader) -> T): T { val attachmentIds = attachments.map(Attachment::id).toSet() - val serializationContext = cache.computeIfAbsent(Key(attachmentIds, params)) { + val cache = attachmentsClassLoaderCache ?: fallBackCache + val serializationContext = cache.computeIfAbsent(AttachmentsClassLoaderKey(attachmentIds, params), Function { // Create classloader and load serializers, whitelisted classes val transactionClassLoader = AttachmentsClassLoader(attachments, params, txId, isAttachmentTrusted, parent) val serializers = try { @@ -336,7 +336,7 @@ object AttachmentsClassLoaderBuilder { .withWhitelist(whitelistedClasses) .withCustomSerializers(serializers) .withoutCarpenter() - } + }) // Deserialize all relevant classes in the transaction classloader. return SerializationFactory.defaultFactory.withCurrentContext(serializationContext) { @@ -420,6 +420,36 @@ private class AttachmentsHolderImpl : AttachmentsHolder { } } +interface AttachmentsClassLoaderCache { + fun computeIfAbsent(key: AttachmentsClassLoaderKey, mappingFunction: Function): SerializationContext +} + +@DeleteForDJVM +class AttachmentsClassLoaderCacheImpl(cacheFactory: NamedCacheFactory) : SingletonSerializeAsToken(), AttachmentsClassLoaderCache { + + private val cache: Cache = cacheFactory.buildNamed(Caffeine.newBuilder(), "AttachmentsClassLoader_cache") + + override fun computeIfAbsent(key: AttachmentsClassLoaderKey, mappingFunction: Function): SerializationContext { + return cache.get(key, mappingFunction) ?: throw NullPointerException("null returned from cache mapping function") + } +} + +class AttachmentsClassLoaderSimpleCacheImpl(cacheSize: Int) : AttachmentsClassLoaderCache { + + private val cache: MutableMap + = createSimpleCache(cacheSize).toSynchronised() + + override fun computeIfAbsent(key: AttachmentsClassLoaderKey, mappingFunction: Function): SerializationContext { + return cache.computeIfAbsent(key, mappingFunction) + } +} + +// We use a set here because the ordering of attachments doesn't affect code execution, due to the no +// overlap rule, and attachments don't have any particular ordering enforced by the builders. So we +// can just do unordered comparisons here. But the same attachments run with different network parameters +// may behave differently, so that has to be a part of the cache key. +data class AttachmentsClassLoaderKey(val hashes: Set, val params: NetworkParameters) + private class AttachmentURLConnection(url: URL, private val attachment: Attachment) : URLConnection(url) { override fun getContentLengthLong(): Long = attachment.size.toLong() override fun getInputStream(): InputStream = attachment.open() diff --git a/core/src/main/kotlin/net/corda/core/transactions/ContractUpgradeTransactions.kt b/core/src/main/kotlin/net/corda/core/transactions/ContractUpgradeTransactions.kt index c590047267..277dccc1d2 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/ContractUpgradeTransactions.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/ContractUpgradeTransactions.kt @@ -153,7 +153,8 @@ data class ContractUpgradeWireTransaction( listOf(legacyAttachment, upgradedAttachment), params, id, - { (services as ServiceHubCoreInternal).attachmentTrustCalculator.calculate(it) }) { transactionClassLoader -> + { (services as ServiceHubCoreInternal).attachmentTrustCalculator.calculate(it) }, + attachmentsClassLoaderCache = (services as ServiceHubCoreInternal).attachmentsClassLoaderCache) { transactionClassLoader -> val resolvedInput = binaryInput.deserialize() val upgradedContract = upgradedContract(upgradedContractClassName, transactionClassLoader) val outputState = calculateUpgradedState(resolvedInput, upgradedContract, upgradedAttachment) diff --git a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt index 3dffc8182c..6c73c299c2 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt @@ -26,6 +26,7 @@ import net.corda.core.internal.deserialiseComponentGroup import net.corda.core.internal.isUploaderTrusted import net.corda.core.internal.uncheckedCast import net.corda.core.node.NetworkParameters +import net.corda.core.serialization.internal.AttachmentsClassLoaderCache import net.corda.core.serialization.internal.AttachmentsClassLoaderBuilder import net.corda.core.utilities.contextLogger import java.util.Collections.unmodifiableList @@ -87,7 +88,8 @@ private constructor( private val serializedInputs: List?, private val serializedReferences: List?, private val isAttachmentTrusted: (Attachment) -> Boolean, - private val verifierFactory: (LedgerTransaction, ClassLoader) -> Verifier + private val verifierFactory: (LedgerTransaction, ClassLoader) -> Verifier, + private val attachmentsClassLoaderCache: AttachmentsClassLoaderCache? ) : FullTransaction() { init { @@ -124,7 +126,8 @@ private constructor( componentGroups: List? = null, serializedInputs: List? = null, serializedReferences: List? = null, - isAttachmentTrusted: (Attachment) -> Boolean + isAttachmentTrusted: (Attachment) -> Boolean, + attachmentsClassLoaderCache: AttachmentsClassLoaderCache? ): LedgerTransaction { return LedgerTransaction( inputs = inputs, @@ -141,7 +144,8 @@ private constructor( serializedInputs = protect(serializedInputs), serializedReferences = protect(serializedReferences), isAttachmentTrusted = isAttachmentTrusted, - verifierFactory = ::BasicVerifier + verifierFactory = ::BasicVerifier, + attachmentsClassLoaderCache = attachmentsClassLoaderCache ) } @@ -176,7 +180,8 @@ private constructor( serializedInputs = null, serializedReferences = null, isAttachmentTrusted = { true }, - verifierFactory = ::BasicVerifier + verifierFactory = ::BasicVerifier, + attachmentsClassLoaderCache = null ) } } @@ -218,7 +223,8 @@ private constructor( txAttachments, getParamsWithGoo(), id, - isAttachmentTrusted = isAttachmentTrusted) { transactionClassLoader -> + isAttachmentTrusted = isAttachmentTrusted, + attachmentsClassLoaderCache = attachmentsClassLoaderCache) { transactionClassLoader -> // Create a copy of the outer LedgerTransaction which deserializes all fields inside the [transactionClassLoader]. // Only the copy will be used for verification, and the outer shell will be discarded. // This artifice is required to preserve backwards compatibility. @@ -254,7 +260,8 @@ private constructor( serializedInputs = serializedInputs, serializedReferences = serializedReferences, isAttachmentTrusted = isAttachmentTrusted, - verifierFactory = alternateVerifier + verifierFactory = alternateVerifier, + attachmentsClassLoaderCache = attachmentsClassLoaderCache ) // Read network parameters with backwards compatibility goo. @@ -320,7 +327,8 @@ private constructor( serializedInputs = serializedInputs, serializedReferences = serializedReferences, isAttachmentTrusted = isAttachmentTrusted, - verifierFactory = verifierFactory + verifierFactory = verifierFactory, + attachmentsClassLoaderCache = attachmentsClassLoaderCache ) } else { // This branch is only present for backwards compatibility. @@ -704,7 +712,8 @@ private constructor( serializedInputs = null, serializedReferences = null, isAttachmentTrusted = { it.isUploaderTrusted() }, - verifierFactory = ::BasicVerifier + verifierFactory = ::BasicVerifier, + attachmentsClassLoaderCache = null ) @Deprecated("LedgerTransaction should not be created directly, use WireTransaction.toLedgerTransaction instead.") @@ -733,7 +742,8 @@ private constructor( serializedInputs = null, serializedReferences = null, isAttachmentTrusted = { it.isUploaderTrusted() }, - verifierFactory = ::BasicVerifier + verifierFactory = ::BasicVerifier, + attachmentsClassLoaderCache = null ) @Deprecated("LedgerTransactions should not be created directly, use WireTransaction.toLedgerTransaction instead.") @@ -761,7 +771,8 @@ private constructor( serializedInputs = serializedInputs, serializedReferences = serializedReferences, isAttachmentTrusted = isAttachmentTrusted, - verifierFactory = verifierFactory + verifierFactory = verifierFactory, + attachmentsClassLoaderCache = attachmentsClassLoaderCache ) } @@ -791,7 +802,8 @@ private constructor( serializedInputs = serializedInputs, serializedReferences = serializedReferences, isAttachmentTrusted = isAttachmentTrusted, - verifierFactory = verifierFactory + verifierFactory = verifierFactory, + attachmentsClassLoaderCache = attachmentsClassLoaderCache ) } } diff --git a/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt index ac7be9afeb..73b286276d 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt @@ -15,6 +15,7 @@ import net.corda.core.node.ServicesForResolution import net.corda.core.node.services.AttachmentId import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SerializedBytes +import net.corda.core.serialization.internal.AttachmentsClassLoaderCache import net.corda.core.serialization.serialize import net.corda.core.utilities.OpaqueBytes import java.security.PublicKey @@ -109,7 +110,8 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr services.networkParametersService.lookup(hashToResolve) }, // `as?` is used due to [MockServices] not implementing [ServiceHubCoreInternal] - isAttachmentTrusted = { (services as? ServiceHubCoreInternal)?.attachmentTrustCalculator?.calculate(it) ?: true } + isAttachmentTrusted = { (services as? ServiceHubCoreInternal)?.attachmentTrustCalculator?.calculate(it) ?: true }, + attachmentsClassLoaderCache = (services as? ServiceHubCoreInternal)?.attachmentsClassLoaderCache ) ) } @@ -145,7 +147,8 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr resolveAttachment, { stateRef -> resolveStateRef(stateRef)?.serialize() }, { null }, - { it.isUploaderTrusted() } + { it.isUploaderTrusted() }, + null ) } @@ -161,16 +164,19 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr resolveAttachment, { stateRef -> resolveStateRef(stateRef)?.serialize() }, resolveParameters, - { true } // Any attachment loaded through the DJVM should be trusted + { true }, // Any attachment loaded through the DJVM should be trusted + null ) } + @Suppress("LongParameterList", "ThrowsCount") private fun toLedgerTransactionInternal( resolveIdentity: (PublicKey) -> Party?, resolveAttachment: (SecureHash) -> Attachment?, resolveStateRefAsSerialized: (StateRef) -> SerializedBytes>?, resolveParameters: (SecureHash?) -> NetworkParameters?, - isAttachmentTrusted: (Attachment) -> Boolean + isAttachmentTrusted: (Attachment) -> Boolean, + attachmentsClassLoaderCache: AttachmentsClassLoaderCache? ): LedgerTransaction { // Look up public keys to authenticated identities. val authenticatedCommands = commands.lazyMapped { cmd, _ -> @@ -206,7 +212,8 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr componentGroups, serializedResolvedInputs, serializedResolvedReferences, - isAttachmentTrusted + isAttachmentTrusted, + attachmentsClassLoaderCache ) checkTransactionSize(ltx, resolvedNetworkParameters.maxTransactionSize, serializedResolvedInputs, serializedResolvedReferences) diff --git a/core/src/test/kotlin/net/corda/core/internal/internalAccessTestHelpers.kt b/core/src/test/kotlin/net/corda/core/internal/internalAccessTestHelpers.kt index 148aaff7bf..c325c805e3 100644 --- a/core/src/test/kotlin/net/corda/core/internal/internalAccessTestHelpers.kt +++ b/core/src/test/kotlin/net/corda/core/internal/internalAccessTestHelpers.kt @@ -4,6 +4,7 @@ import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash import net.corda.core.identity.Party import net.corda.core.node.NetworkParameters +import net.corda.core.serialization.internal.AttachmentsClassLoaderCache import net.corda.core.transactions.ComponentGroup import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.WireTransaction @@ -17,6 +18,7 @@ fun WireTransaction.accessGroupHashes() = this.groupHashes fun WireTransaction.accessGroupMerkleRoots() = this.groupsMerkleRoots fun WireTransaction.accessAvailableComponentHashes() = this.availableComponentHashes +@Suppress("LongParameterList") fun createLedgerTransaction( inputs: List>, outputs: List>, @@ -31,8 +33,9 @@ fun createLedgerTransaction( componentGroups: List? = null, serializedInputs: List? = null, serializedReferences: List? = null, - isAttachmentTrusted: (Attachment) -> Boolean -): LedgerTransaction = LedgerTransaction.create(inputs, outputs, commands, attachments, id, notary, timeWindow, privacySalt, networkParameters, references, componentGroups, serializedInputs, serializedReferences, isAttachmentTrusted) + isAttachmentTrusted: (Attachment) -> Boolean, + attachmentsClassLoaderCache: AttachmentsClassLoaderCache +): LedgerTransaction = LedgerTransaction.create(inputs, outputs, commands, attachments, id, notary, timeWindow, privacySalt, networkParameters, references, componentGroups, serializedInputs, serializedReferences, isAttachmentTrusted, attachmentsClassLoaderCache) fun createContractCreationError(txId: SecureHash, contractClass: String, cause: Throwable) = TransactionVerificationException.ContractCreationError(txId, contractClass, cause) fun createContractRejection(txId: SecureHash, contract: Contract, cause: Throwable) = TransactionVerificationException.ContractRejection(txId, contract, cause) diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 74dcee8c01..c115fd45a4 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -59,6 +59,8 @@ import net.corda.core.schemas.MappedSchema import net.corda.core.serialization.SerializationWhitelist import net.corda.core.serialization.SerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken +import net.corda.core.serialization.internal.AttachmentsClassLoaderCache +import net.corda.core.serialization.internal.AttachmentsClassLoaderCacheImpl import net.corda.core.toFuture import net.corda.core.transactions.LedgerTransaction import net.corda.core.utilities.NetworkHostAndPort @@ -317,6 +319,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, } else { BasicVerifierFactoryService() } + private val attachmentsClassLoaderCache: AttachmentsClassLoaderCache = AttachmentsClassLoaderCacheImpl(cacheFactory).tokenize() val contractUpgradeService = ContractUpgradeServiceImpl(cacheFactory).tokenize() val auditService = DummyAuditService().tokenize() @Suppress("LeakingThis") @@ -1171,6 +1174,8 @@ abstract class AbstractNode(val configuration: NodeConfiguration, private lateinit var _myInfo: NodeInfo override val myInfo: NodeInfo get() = _myInfo + override val attachmentsClassLoaderCache: AttachmentsClassLoaderCache get() = this@AbstractNode.attachmentsClassLoaderCache + private lateinit var _networkParameters: NetworkParameters override val networkParameters: NetworkParameters get() = _networkParameters diff --git a/node/src/main/kotlin/net/corda/node/migration/MigrationNamedCacheFactory.kt b/node/src/main/kotlin/net/corda/node/migration/MigrationNamedCacheFactory.kt index 70bb911106..0e2538e8ac 100644 --- a/node/src/main/kotlin/net/corda/node/migration/MigrationNamedCacheFactory.kt +++ b/node/src/main/kotlin/net/corda/node/migration/MigrationNamedCacheFactory.kt @@ -37,6 +37,7 @@ class MigrationNamedCacheFactory(private val metricRegistry: MetricRegistry?, "NodeAttachmentService_contractAttachmentVersions" -> caffeine.maximumSize(defaultCacheSize) "NodeParametersStorage_networkParametersByHash" -> caffeine.maximumSize(defaultCacheSize) "NodeAttachmentTrustCalculator_trustedKeysCache" -> caffeine.maximumSize(defaultCacheSize) + "AttachmentsClassLoader_cache" -> caffeine.maximumSize(defaultCacheSize) else -> throw IllegalArgumentException("Unexpected cache name $name.") } } diff --git a/node/src/main/kotlin/net/corda/node/migration/MigrationServicesForResolution.kt b/node/src/main/kotlin/net/corda/node/migration/MigrationServicesForResolution.kt index ec8b5d315d..0186b9659c 100644 --- a/node/src/main/kotlin/net/corda/node/migration/MigrationServicesForResolution.kt +++ b/node/src/main/kotlin/net/corda/node/migration/MigrationServicesForResolution.kt @@ -15,6 +15,8 @@ import net.corda.core.node.services.NetworkParametersService import net.corda.core.node.services.TransactionStorage import net.corda.core.serialization.deserialize import net.corda.core.serialization.internal.AttachmentsClassLoaderBuilder +import net.corda.core.serialization.internal.AttachmentsClassLoaderCache +import net.corda.core.serialization.internal.AttachmentsClassLoaderCacheImpl import net.corda.core.transactions.ContractUpgradeLedgerTransaction import net.corda.core.transactions.NotaryChangeLedgerTransaction import net.corda.core.transactions.WireTransaction @@ -62,6 +64,8 @@ class MigrationServicesForResolution( cacheFactory ) + private val attachmentsClassLoaderCache: AttachmentsClassLoaderCache = AttachmentsClassLoaderCacheImpl(cacheFactory) + private fun defaultNetworkParameters(): NetworkParameters { logger.warn("Using a dummy set of network parameters for migration.") val clock = Clock.systemUTC() @@ -124,7 +128,8 @@ class MigrationServicesForResolution( networkParameters, tx.id, attachmentTrustCalculator::calculate, - cordappLoader.appClassLoader) { + cordappLoader.appClassLoader, + attachmentsClassLoaderCache) { deserialiseComponentGroup(tx.componentGroups, TransactionState::class, ComponentGroupEnum.OUTPUTS_GROUP, forceDeserialize = true) } states.filterIndexed {index, _ -> stateIndices.contains(index)}.toList() diff --git a/node/src/main/kotlin/net/corda/node/utilities/NodeNamedCache.kt b/node/src/main/kotlin/net/corda/node/utilities/NodeNamedCache.kt index 4a514e0172..4d6dd05b19 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/NodeNamedCache.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/NodeNamedCache.kt @@ -63,6 +63,7 @@ open class DefaultNamedCacheFactory protected constructor(private val metricRegi name == "NodeParametersStorage_networkParametersByHash" -> caffeine.maximumSize(defaultCacheSize) name == "PublicKeyToOwningIdentityCache_cache" -> caffeine.maximumSize(defaultCacheSize) name == "NodeAttachmentTrustCalculator_trustedKeysCache" -> caffeine.maximumSize(defaultCacheSize) + name == "AttachmentsClassLoader_cache" -> caffeine.maximumSize(defaultAttachmentsClassLoaderCacheSize) else -> throw IllegalArgumentException("Unexpected cache name $name. Did you add a new cache?") } } @@ -85,4 +86,6 @@ open class DefaultNamedCacheFactory protected constructor(private val metricRegi } open protected val defaultCacheSize = 1024L -} \ No newline at end of file + private val defaultAttachmentsClassLoaderCacheSize = defaultCacheSize / CACHE_SIZE_DENOMINATOR +} +private const val CACHE_SIZE_DENOMINATOR = 4L \ No newline at end of file diff --git a/node/src/test/kotlin/net/corda/node/services/FinalityHandlerTest.kt b/node/src/test/kotlin/net/corda/node/services/FinalityHandlerTest.kt index 594932f5c0..b6bb0817b2 100644 --- a/node/src/test/kotlin/net/corda/node/services/FinalityHandlerTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/FinalityHandlerTest.kt @@ -50,13 +50,13 @@ class FinalityHandlerTest { getOrThrow() } - bob.assertFlowSentForObservationDueToConstraintError(finalityHandlerId) + bob.assertFlowSentForObservationDueToUntrustedAttachmentsException(finalityHandlerId) assertThat(bob.getTransaction(stx.id)).isNull() bob = mockNet.restartNode(bob) // Since we've not done anything to fix the orignal error, we expect the finality handler to be sent to the hospital // again on restart - bob.assertFlowSentForObservationDueToConstraintError(finalityHandlerId) + bob.assertFlowSentForObservationDueToUntrustedAttachmentsException(finalityHandlerId) assertThat(bob.getTransaction(stx.id)).isNull() } @@ -96,7 +96,7 @@ class FinalityHandlerTest { .ofType(R::class.java) } - private fun TestStartedNode.assertFlowSentForObservationDueToConstraintError(runId: StateMachineRunId) { + private fun TestStartedNode.assertFlowSentForObservationDueToUntrustedAttachmentsException(runId: StateMachineRunId) { val observation = medicalRecordsOfType() .filter { it.flowId == runId } .toBlocking() @@ -104,7 +104,7 @@ class FinalityHandlerTest { assertThat(observation.outcome).isEqualTo(Outcome.OVERNIGHT_OBSERVATION) assertThat(observation.by).contains(FinalityDoctor) val error = observation.errors.single() - assertThat(error).isInstanceOf(TransactionVerificationException.ContractConstraintRejection::class.java) + assertThat(error).isInstanceOf(TransactionVerificationException.UntrustedAttachmentsException::class.java) } private fun TestStartedNode.getTransaction(id: SecureHash): SignedTransaction? { diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt index fa3ec67c93..9a25595d63 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt @@ -14,6 +14,8 @@ import net.corda.core.node.ServiceHub import net.corda.core.node.ServicesForResolution import net.corda.core.node.services.AttachmentId import net.corda.core.node.services.TransactionStorage +import net.corda.core.serialization.internal.AttachmentsClassLoaderCache +import net.corda.core.serialization.internal.AttachmentsClassLoaderCacheImpl import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.WireTransaction @@ -130,6 +132,8 @@ data class TestTransactionDSLInterpreter private constructor( ledgerInterpreter.services.cordappProvider override val notaryService: NotaryService? = null + + override val attachmentsClassLoaderCache: AttachmentsClassLoaderCache = AttachmentsClassLoaderCacheImpl(TestingNamedCacheFactory()) } private fun copy(): TestTransactionDSLInterpreter = diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/TestingNamedCacheFactory.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/TestingNamedCacheFactory.kt index 6802fd042e..402b3757c0 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/TestingNamedCacheFactory.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/TestingNamedCacheFactory.kt @@ -26,6 +26,7 @@ class TestingNamedCacheFactory private constructor(private val sizeOverride: Lon val configuredCaffeine = when (name) { "DBTransactionStorage_transactions" -> caffeine.maximumWeight(1.MB) "NodeAttachmentService_attachmentContent" -> caffeine.maximumWeight(1.MB) + "AttachmentsClassLoader_cache" -> caffeine.maximumSize(sizeOverride) else -> caffeine.maximumSize(sizeOverride) } return configuredCaffeine.build(loader) From 1660e7674b465f64648ae098a5c4bce0d8ded749 Mon Sep 17 00:00:00 2001 From: Euan Cairncross <35581327+cairncross@users.noreply.github.com> Date: Thu, 16 Jul 2020 10:46:42 +0100 Subject: [PATCH 74/85] INFRA-270 Publish archived API docs to Artifactory when tagged (#6309) * Reintroduce `build.gradle` from 4.4 * Add Jenkins publication logic Co-authored-by: Waldemar Zurowski Co-authored-by: Ross Nicoll --- .ci/dev/publish-api-docs/Jenkinsfile | 35 ++++++++ docs/build.gradle | 122 +++++++++++++++++++++++++++ 2 files changed, 157 insertions(+) create mode 100644 .ci/dev/publish-api-docs/Jenkinsfile create mode 100644 docs/build.gradle diff --git a/.ci/dev/publish-api-docs/Jenkinsfile b/.ci/dev/publish-api-docs/Jenkinsfile new file mode 100644 index 0000000000..d99d17ef44 --- /dev/null +++ b/.ci/dev/publish-api-docs/Jenkinsfile @@ -0,0 +1,35 @@ +@Library('corda-shared-build-pipeline-steps') + +import static com.r3.build.BuildControl.killAllExistingBuildsForJob + +killAllExistingBuildsForJob(env.JOB_NAME, env.BUILD_NUMBER.toInteger()) + +pipeline { + agent { label 'standard' } + options { + ansiColor('xterm') + timestamps() + timeout(time: 3, unit: 'HOURS') + } + + environment { + ARTIFACTORY_CREDENTIALS = credentials('artifactory-credentials') + CORDA_ARTIFACTORY_USERNAME = "${env.ARTIFACTORY_CREDENTIALS_USR}" + CORDA_ARTIFACTORY_PASSWORD = "${env.ARTIFACTORY_CREDENTIALS_PSW}" + } + + stages { + stage('Publish Archived API Docs to Artifactory') { + when { tag pattern: /^release-os-V(\d+\.\d+)(\.\d+){0,1}(-GA){0,1}(-\d{4}-\d\d-\d\d-\d{4}){0,1}$/, comparator: 'REGEXP' } + steps { + sh "./gradlew :clean :docs:artifactoryPublish -DpublishApiDocs" + } + } + } + + post { + cleanup { + deleteDir() /* clean up our workspace */ + } + } +} diff --git a/docs/build.gradle b/docs/build.gradle new file mode 100644 index 0000000000..09bdac83bc --- /dev/null +++ b/docs/build.gradle @@ -0,0 +1,122 @@ +import org.apache.tools.ant.taskdefs.condition.Os + +apply plugin: 'org.jetbrains.dokka' +apply plugin: 'net.corda.plugins.publish-utils' +apply plugin: 'maven-publish' +apply plugin: 'com.jfrog.artifactory' + +def internalPackagePrefixes(sourceDirs) { + def prefixes = [] + // Kotlin allows packages to deviate from the directory structure, but let's assume they don't: + sourceDirs.collect { sourceDir -> + sourceDir.traverse(type: groovy.io.FileType.DIRECTORIES) { + if (it.name == 'internal') { + prefixes.add sourceDir.toPath().relativize(it.toPath()).toString().replace(File.separator, '.') + } + } + } + prefixes +} + +ext { + // TODO: Add '../client/jfx/src/main/kotlin' and '../client/mock/src/main/kotlin' if we decide to make them into public API + dokkaSourceDirs = files('../core/src/main/kotlin', '../client/rpc/src/main/kotlin', '../finance/workflows/src/main/kotlin', '../finance/contracts/src/main/kotlin', '../client/jackson/src/main/kotlin', + '../testing/test-utils/src/main/kotlin', '../testing/node-driver/src/main/kotlin') + internalPackagePrefixes = internalPackagePrefixes(dokkaSourceDirs) + archivedApiDocsBaseFilename = 'api-docs' +} + +dokka { + outputDirectory = file("${rootProject.rootDir}/docs/build/html/api/kotlin") +} + +task dokkaJavadoc(type: org.jetbrains.dokka.gradle.DokkaTask) { + outputFormat = "javadoc" + outputDirectory = file("${rootProject.rootDir}/docs/build/html/api/javadoc") +} + +[dokka, dokkaJavadoc].collect { + it.configure { + moduleName = 'corda' + processConfigurations = ['compile'] + sourceDirs = dokkaSourceDirs + includes = ['packages.md'] + jdkVersion = 8 + externalDocumentationLink { + url = new URL("http://fasterxml.github.io/jackson-core/javadoc/2.9/") + } + externalDocumentationLink { + url = new URL("https://docs.oracle.com/javafx/2/api/") + } + externalDocumentationLink { + url = new URL("http://www.bouncycastle.org/docs/docs1.5on/") + } + internalPackagePrefixes.collect { packagePrefix -> + packageOptions { + prefix = packagePrefix + suppress = true + } + } + } +} + +task apidocs(dependsOn: ['dokka', 'dokkaJavadoc']) { + group "Documentation" + description "Build API documentation" +} + +task makeHTMLDocs(type: Exec){ + if (Os.isFamily(Os.FAMILY_WINDOWS)) { + commandLine "docker", "run", "--rm", "-v", "${project.projectDir}:/opt/docs_builder", "-v", "${project.projectDir}/..:/opt", "corda/docs-builder:latest", "bash", "-c", "make-docsite-html.sh" + } else { + commandLine "bash", "-c", "docker run --rm --user \$(id -u):\$(id -g) -v ${project.projectDir}:/opt/docs_builder -v ${project.projectDir}/..:/opt corda/docs-builder:latest bash -c make-docsite-html.sh" + } +} + +task makePDFDocs(type: Exec){ + if (Os.isFamily(Os.FAMILY_WINDOWS)) { + commandLine "docker", "run", "--rm", "-v", "${project.projectDir}:/opt/docs_builder", "-v", "${project.projectDir}/..:/opt", "corda/docs-builder:latest", "bash", "-c", "make-docsite-pdf.sh" + } else { + commandLine "bash", "-c", "docker run --rm --user \$(id -u):\$(id -g) -v ${project.projectDir}:/opt/docs_builder -v ${project.projectDir}/..:/opt corda/docs-builder:latest bash -c make-docsite-pdf.sh" + } +} + +task makeDocs(dependsOn: ['makeHTMLDocs', 'makePDFDocs']) +apidocs.shouldRunAfter makeDocs + +task archiveApiDocs(type: Tar) { + dependsOn apidocs + from buildDir + include 'html/**' + extension 'tgz' + compression Compression.GZIP +} + +publishing { + publications { + if (System.getProperty('publishApiDocs') != null) { + archivedApiDocs(MavenPublication) { + artifact archiveApiDocs { + artifactId archivedApiDocsBaseFilename + } + } + } + } +} + +artifactoryPublish { + publications('archivedApiDocs') + version = version.replaceAll('-SNAPSHOT', '') + publishPom = false +} + +artifactory { + publish { + contextUrl = artifactory_contextUrl + repository { + repoKey = 'corda-dependencies-dev' + username = System.getenv('CORDA_ARTIFACTORY_USERNAME') + password = System.getenv('CORDA_ARTIFACTORY_PASSWORD') + } + } +} From fad8999ef4b3b1c15a2e5f398b6ac5f3e2b7878c Mon Sep 17 00:00:00 2001 From: Viktor Kolomeyko Date: Thu, 16 Jul 2020 10:51:23 +0100 Subject: [PATCH 75/85] CORDA-3862: Make SharedMemoryIncremental public (#6467) --- .../java/net/corda/testing/driver/SharedMemoryIncremental.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/node-driver/src/main/java/net/corda/testing/driver/SharedMemoryIncremental.java b/testing/node-driver/src/main/java/net/corda/testing/driver/SharedMemoryIncremental.java index fe0d957499..ae5a927186 100644 --- a/testing/node-driver/src/main/java/net/corda/testing/driver/SharedMemoryIncremental.java +++ b/testing/node-driver/src/main/java/net/corda/testing/driver/SharedMemoryIncremental.java @@ -17,7 +17,7 @@ import java.nio.channels.FileChannel; * import sun.misc.Unsafe; * import sun.nio.ch.DirectBuffer; */ -class SharedMemoryIncremental extends PortAllocation { +public class SharedMemoryIncremental extends PortAllocation { static private final int DEFAULT_START_PORT = 10_000; static private final int FIRST_EPHEMERAL_PORT = 30_000; From 9b77f9a170b09567f71622b73b9de8f3ad35e654 Mon Sep 17 00:00:00 2001 From: Razvan Codreanu <52859362+Schife@users.noreply.github.com> Date: Thu, 16 Jul 2020 16:46:10 +0100 Subject: [PATCH 76/85] INFRA-435 Don't publish internal releases to Docker (#6475) --- .ci/dev/regression/Jenkinsfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.ci/dev/regression/Jenkinsfile b/.ci/dev/regression/Jenkinsfile index 0361303e5b..85a699180a 100644 --- a/.ci/dev/regression/Jenkinsfile +++ b/.ci/dev/regression/Jenkinsfile @@ -18,6 +18,7 @@ killAllExistingBuildsForJob(env.JOB_NAME, env.BUILD_NUMBER.toInteger()) * Sense environment */ boolean isReleaseTag = (env.TAG_NAME =~ /^release-.*(? Date: Fri, 17 Jul 2020 09:39:45 +0100 Subject: [PATCH 77/85] INFRA-330 Use Artifactory as cache for all dependencies (#6253) * Use a virtual repo (corda-remotes) containing all Corda repositories with dependencies * activated when CORDA_USE_CACHE environment variable is set * Update Jenkins configuration to use new functionality * it does *not* affect local builds as long as environment variable is not set! --- .ci/dev/compatibility/JenkinsfileJDK11Azul | 8 ++- .ci/dev/nightly-regression/Jenkinsfile | 8 ++- .ci/dev/regression/Jenkinsfile | 10 +++- Jenkinsfile | 8 ++- build.gradle | 66 +++++++++++++++++----- settings.gradle | 24 +++++++- 6 files changed, 100 insertions(+), 24 deletions(-) diff --git a/.ci/dev/compatibility/JenkinsfileJDK11Azul b/.ci/dev/compatibility/JenkinsfileJDK11Azul index 4f04153ded..d24a3f7ac4 100644 --- a/.ci/dev/compatibility/JenkinsfileJDK11Azul +++ b/.ci/dev/compatibility/JenkinsfileJDK11Azul @@ -37,6 +37,9 @@ pipeline { BUILD_ID = "${env.BUILD_ID}-${env.JOB_NAME}" ARTIFACTORY_CREDENTIALS = credentials('artifactory-credentials') ARTIFACTORY_BUILD_NAME = "Corda / Publish / Publish JDK 11 Release to Artifactory".replaceAll("/", "::") + CORDA_USE_CACHE = "corda-remotes" + CORDA_ARTIFACTORY_USERNAME = "${env.ARTIFACTORY_CREDENTIALS_USR}" + CORDA_ARTIFACTORY_PASSWORD = "${env.ARTIFACTORY_CREDENTIALS_PSW}" } stages { @@ -68,6 +71,9 @@ pipeline { "-Ddocker.work.dir=\"/tmp/\${EXECUTOR_NUMBER}\" " + "-Ddocker.build.tag=\"\${DOCKER_TAG_TO_USE}\" " + "-Ddocker.buildbase.tag=11latest " + + "-Ddocker.container.env.parameter.CORDA_USE_CACHE=\"${CORDA_USE_CACHE}\" " + + "-Ddocker.container.env.parameter.CORDA_ARTIFACTORY_USERNAME=\"\${ARTIFACTORY_CREDENTIALS_USR}\" " + + "-Ddocker.container.env.parameter.CORDA_ARTIFACTORY_PASSWORD=\"\${ARTIFACTORY_CREDENTIALS_PSW}\" " + "-Ddocker.dockerfile=DockerfileJDK11Azul" + " clean pushBuildImage preAllocateForParallelRegressionTest preAllocateForAllParallelSlowIntegrationTest --stacktrace" } @@ -147,7 +153,7 @@ pipeline { post { always { archiveArtifacts artifacts: '**/pod-logs/**/*.log', fingerprint: false - junit '**/build/test-results-xml/**/*.xml' + junit testResults: '**/build/test-results-xml/**/*.xml', allowEmptyResults: true } cleanup { deleteDir() /* clean up our workspace */ diff --git a/.ci/dev/nightly-regression/Jenkinsfile b/.ci/dev/nightly-regression/Jenkinsfile index 5c2af45b99..c8d5b0e73a 100644 --- a/.ci/dev/nightly-regression/Jenkinsfile +++ b/.ci/dev/nightly-regression/Jenkinsfile @@ -20,6 +20,9 @@ pipeline { EXECUTOR_NUMBER = "${env.EXECUTOR_NUMBER}" BUILD_ID = "${env.BUILD_ID}-${env.JOB_NAME}" ARTIFACTORY_CREDENTIALS = credentials('artifactory-credentials') + CORDA_USE_CACHE = "corda-remotes" + CORDA_ARTIFACTORY_USERNAME = "${env.ARTIFACTORY_CREDENTIALS_USR}" + CORDA_ARTIFACTORY_PASSWORD = "${env.ARTIFACTORY_CREDENTIALS_PSW}" } stages { @@ -36,6 +39,9 @@ pipeline { "-Dkubenetize=true " + "-Ddocker.push.password=\"\${DOCKER_PUSH_PWD}\" " + "-Ddocker.work.dir=\"/tmp/\${EXECUTOR_NUMBER}\" " + + "-Ddocker.container.env.parameter.CORDA_USE_CACHE=\"${CORDA_USE_CACHE}\" " + + "-Ddocker.container.env.parameter.CORDA_ARTIFACTORY_USERNAME=\"\${ARTIFACTORY_CREDENTIALS_USR}\" " + + "-Ddocker.container.env.parameter.CORDA_ARTIFACTORY_PASSWORD=\"\${ARTIFACTORY_CREDENTIALS_PSW}\" " + "-Ddocker.build.tag=\"\${DOCKER_TAG_TO_USE}\"" + " clean pushBuildImage --stacktrace" } @@ -75,7 +81,6 @@ pipeline { } } - post { always { archiveArtifacts artifacts: '**/pod-logs/**/*.log', fingerprint: false @@ -86,4 +91,3 @@ pipeline { } } } - diff --git a/.ci/dev/regression/Jenkinsfile b/.ci/dev/regression/Jenkinsfile index 66af41226a..77aad30eeb 100644 --- a/.ci/dev/regression/Jenkinsfile +++ b/.ci/dev/regression/Jenkinsfile @@ -48,6 +48,9 @@ pipeline { BUILD_ID = "${env.BUILD_ID}-${env.JOB_NAME}" ARTIFACTORY_CREDENTIALS = credentials('artifactory-credentials') ARTIFACTORY_BUILD_NAME = "Corda / Publish / Publish Release to Artifactory".replaceAll("/", "::") + CORDA_USE_CACHE = "corda-remotes" + CORDA_ARTIFACTORY_USERNAME = "${env.ARTIFACTORY_CREDENTIALS_USR}" + CORDA_ARTIFACTORY_PASSWORD = "${env.ARTIFACTORY_CREDENTIALS_PSW}" } stages { @@ -83,6 +86,9 @@ pipeline { "-Dkubenetize=true " + "-Ddocker.push.password=\"\${DOCKER_PUSH_PWD}\" " + "-Ddocker.work.dir=\"/tmp/\${EXECUTOR_NUMBER}\" " + + "-Ddocker.container.env.parameter.CORDA_USE_CACHE=\"${CORDA_USE_CACHE}\" " + + "-Ddocker.container.env.parameter.CORDA_ARTIFACTORY_USERNAME=\"\${ARTIFACTORY_CREDENTIALS_USR}\" " + + "-Ddocker.container.env.parameter.CORDA_ARTIFACTORY_PASSWORD=\"\${ARTIFACTORY_CREDENTIALS_PSW}\" " + "-Ddocker.build.tag=\"\${DOCKER_TAG_TO_USE}\"" + " clean preAllocateForParallelRegressionTest preAllocateForAllParallelSlowIntegrationTest pushBuildImage --stacktrace" } @@ -170,7 +176,7 @@ pipeline { post { always { archiveArtifacts artifacts: '**/pod-logs/**/*.log', fingerprint: false - junit testResults: '**/build/test-results-xml/**/*.xml', keepLongStdio: true + junit testResults: '**/build/test-results-xml/**/*.xml', keepLongStdio: true, allowEmptyResults: true script { try { @@ -243,4 +249,4 @@ pipeline { deleteDir() /* clean up our workspace */ } } -} \ No newline at end of file +} diff --git a/Jenkinsfile b/Jenkinsfile index fc2c701c29..c27f461148 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -17,6 +17,9 @@ pipeline { EXECUTOR_NUMBER = "${env.EXECUTOR_NUMBER}" BUILD_ID = "${env.BUILD_ID}-${env.JOB_NAME}" ARTIFACTORY_CREDENTIALS = credentials('artifactory-credentials') + CORDA_USE_CACHE = "corda-remotes" + CORDA_ARTIFACTORY_USERNAME = "${env.ARTIFACTORY_CREDENTIALS_USR}" + CORDA_ARTIFACTORY_PASSWORD = "${env.ARTIFACTORY_CREDENTIALS_PSW}" } stages { @@ -27,6 +30,9 @@ pipeline { "-Dkubenetize=true " + "-Ddocker.push.password=\"\${DOCKER_PUSH_PWD}\" " + "-Ddocker.work.dir=\"/tmp/\${EXECUTOR_NUMBER}\" " + + "-Ddocker.container.env.parameter.CORDA_USE_CACHE=\"${CORDA_USE_CACHE}\" " + + "-Ddocker.container.env.parameter.CORDA_ARTIFACTORY_USERNAME=\"\${ARTIFACTORY_CREDENTIALS_USR}\" " + + "-Ddocker.container.env.parameter.CORDA_ARTIFACTORY_PASSWORD=\"\${ARTIFACTORY_CREDENTIALS_PSW}\" " + "-Ddocker.build.tag=\"\${DOCKER_TAG_TO_USE}\"" + " clean preAllocateForAllParallelUnitTest preAllocateForAllParallelIntegrationTest pushBuildImage --stacktrace" } @@ -73,7 +79,7 @@ pipeline { post { always { archiveArtifacts artifacts: '**/pod-logs/**/*.log', fingerprint: false - junit testResults: '**/build/test-results-xml/**/*.xml', keepLongStdio: true + junit testResults: '**/build/test-results-xml/**/*.xml', keepLongStdio: true, allowEmptyResults: true } cleanup { deleteDir() /* clean up our workspace */ diff --git a/build.gradle b/build.gradle index e7d5dd3c84..4ecd0a9f8f 100644 --- a/build.gradle +++ b/build.gradle @@ -155,16 +155,34 @@ buildscript { ext.corda_docs_link = "https://docs.corda.net/docs/corda-os/$baseVersion" repositories { mavenLocal() - mavenCentral() - jcenter() - maven { - url 'https://kotlin.bintray.com/kotlinx' - } - maven { - url "$artifactory_contextUrl/corda-dependencies-dev" - } - maven { - url "$artifactory_contextUrl/corda-releases" + // Use system environment to activate caching with Artifactory, + // because it is actually easier to pass that during parallel build. + // NOTE: it has to be a name of a virtual repository with all + // required remote or local repositories! + if (System.getenv("CORDA_USE_CACHE")) { + maven { + name "R3 Maven remote repositories" + url "${artifactory_contextUrl}/${System.getenv("CORDA_USE_CACHE")}" + authentication { + basic(BasicAuthentication) + } + credentials { + username = System.getenv('CORDA_ARTIFACTORY_USERNAME') + password = System.getenv('CORDA_ARTIFACTORY_PASSWORD') + } + } + } else { + mavenCentral() + jcenter() + maven { + url 'https://kotlin.bintray.com/kotlinx' + } + maven { + url "${artifactory_contextUrl}/corda-dependencies-dev" + } + maven { + url "${artifactory_contextUrl}/corda-releases" + } } } dependencies { @@ -357,11 +375,29 @@ allprojects { repositories { mavenLocal() - mavenCentral() - jcenter() - maven { url "$artifactory_contextUrl/corda-dependencies" } - maven { url 'https://repo.gradle.org/gradle/libs-releases' } - maven { url "$artifactory_contextUrl/corda-dev" } + // Use system environment to activate caching with Artifactory, + // because it is actually easier to pass that during parallel build. + // NOTE: it has to be a name of a virtual repository with all + // required remote or local repositories! + if (System.getenv("CORDA_USE_CACHE")) { + maven { + name "R3 Maven remote repositories" + url "${artifactory_contextUrl}/${System.getenv("CORDA_USE_CACHE")}" + authentication { + basic(BasicAuthentication) + } + credentials { + username = System.getenv('CORDA_ARTIFACTORY_USERNAME') + password = System.getenv('CORDA_ARTIFACTORY_PASSWORD') + } + } + } else { + mavenCentral() + jcenter() + maven { url "${artifactory_contextUrl}/corda-dependencies" } + maven { url 'https://repo.gradle.org/gradle/libs-releases' } + maven { url "${artifactory_contextUrl}/corda-dev" } + } } configurations { diff --git a/settings.gradle b/settings.gradle index ae6dad0838..05bb5040fc 100644 --- a/settings.gradle +++ b/settings.gradle @@ -2,9 +2,27 @@ pluginManagement { ext.artifactory_contextUrl = 'https://software.r3.com/artifactory' repositories { - mavenLocal() - gradlePluginPortal() - maven { url "$artifactory_contextUrl/corda-dependencies" } + // Use system environment to activate caching with Artifactory, + // because it is actually easier to pass that during parallel build. + // NOTE: it has to be a name of a virtual repository with all + // required remote or local repositories! + if (System.getenv("CORDA_USE_CACHE")) { + maven { + name "R3 Maven remote repositories" + url "${artifactory_contextUrl}/${System.getenv("CORDA_USE_CACHE")}" + authentication { + basic(BasicAuthentication) + } + credentials { + username = System.getenv('CORDA_ARTIFACTORY_USERNAME') + password = System.getenv('CORDA_ARTIFACTORY_PASSWORD') + } + } + } else { + mavenLocal() + gradlePluginPortal() + maven { url "${artifactory_contextUrl}/corda-dependencies" } + } } } // The project is named 'corda-project' and not 'corda' because if this is named the same as the From 5d7060ec3a0f53285595a96719815c79010e3d2d Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Fri, 17 Jul 2020 11:26:51 +0000 Subject: [PATCH 78/85] CORDA-3901 Eliminate random reconnect test (#6446) Remove a legacy test for RPC reconnection, which takes 5 minutes to run a random set of tests. This is expensive and low value. --- .../node/services/rpc/RpcReconnectTests.kt | 355 ------------------ 1 file changed, 355 deletions(-) delete mode 100644 node/src/integration-test-slow/kotlin/net/corda/node/services/rpc/RpcReconnectTests.kt diff --git a/node/src/integration-test-slow/kotlin/net/corda/node/services/rpc/RpcReconnectTests.kt b/node/src/integration-test-slow/kotlin/net/corda/node/services/rpc/RpcReconnectTests.kt deleted file mode 100644 index 242cacdaad..0000000000 --- a/node/src/integration-test-slow/kotlin/net/corda/node/services/rpc/RpcReconnectTests.kt +++ /dev/null @@ -1,355 +0,0 @@ -package net.corda.node.services.rpc - -import net.corda.client.rpc.CordaRPCClient -import net.corda.client.rpc.CordaRPCClientConfiguration -import net.corda.client.rpc.GracefulReconnect -import net.corda.client.rpc.internal.ReconnectingCordaRPCOps -import net.corda.client.rpc.notUsed -import net.corda.core.contracts.Amount -import net.corda.core.flows.StateMachineRunId -import net.corda.core.internal.concurrent.transpose -import net.corda.core.messaging.StateMachineUpdate -import net.corda.core.node.services.Vault -import net.corda.core.node.services.vault.PageSpecification -import net.corda.core.node.services.vault.QueryCriteria -import net.corda.core.node.services.vault.builder -import net.corda.core.utilities.NetworkHostAndPort -import net.corda.core.utilities.OpaqueBytes -import net.corda.core.utilities.contextLogger -import net.corda.core.utilities.getOrThrow -import net.corda.core.utilities.seconds -import net.corda.finance.contracts.asset.Cash -import net.corda.finance.flows.CashIssueAndPaymentFlow -import net.corda.finance.schemas.CashSchemaV1 -import net.corda.node.services.Permissions -import net.corda.node.services.rpc.RpcReconnectTests.Companion.NUMBER_OF_FLOWS_TO_RUN -import net.corda.testing.core.DUMMY_BANK_A_NAME -import net.corda.testing.core.DUMMY_BANK_B_NAME -import net.corda.testing.driver.DriverParameters -import net.corda.testing.driver.NodeHandle -import net.corda.testing.driver.OutOfProcess -import net.corda.testing.driver.driver -import net.corda.testing.driver.internal.OutOfProcessImpl -import net.corda.testing.driver.internal.incrementalPortAllocation -import net.corda.testing.node.User -import net.corda.testing.node.internal.FINANCE_CORDAPPS -import org.assertj.core.api.Assertions.assertThat -import org.junit.Test -import java.util.* -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit -import java.util.concurrent.atomic.AtomicInteger -import kotlin.concurrent.thread -import kotlin.math.absoluteValue -import kotlin.math.max -import kotlin.test.assertEquals -import kotlin.test.assertTrue -import kotlin.test.currentStackTrace - -/** - * This is a stress test for the rpc reconnection logic, which triggers failures in a probabilistic way. - * - * You can adjust the variable [NUMBER_OF_FLOWS_TO_RUN] to adjust the number of flows to run and the duration of the test. - */ -class RpcReconnectTests { - - companion object { - // this many flows take ~5 minutes - const val NUMBER_OF_FLOWS_TO_RUN = 100 - - private val log = contextLogger() - } - - private val portAllocator = incrementalPortAllocation() - - private lateinit var proxy: RandomFailingProxy - private lateinit var node: NodeHandle - private lateinit var currentAddressPair: AddressPair - - /** - * This test showcases and stress tests the demo [ReconnectingCordaRPCOps]. - * - * Note that during node failure events can be lost and starting flows can become unreliable. - * The only available way to retry failed flows is to attempt a "logical retry" which is also showcased. - * - * This test runs flows in a loop and in the background kills the node or restarts it. - * Also the RPC connection is made through a proxy that introduces random latencies and is also periodically killed. - */ - @Suppress("ComplexMethod") - @Test(timeout=420_000) - fun `test that the RPC client is able to reconnect and proceed after node failure, restart, or connection reset`() { - val nodeRunningTime = { Random().nextInt(12000) + 8000 } - - val demoUser = User("demo", "demo", setOf(Permissions.all())) - - // When this reaches 0 - the test will end. - val flowsCountdownLatch = CountDownLatch(NUMBER_OF_FLOWS_TO_RUN) - // These are the expected progress steps for the CashIssueAndPayFlow. - val expectedProgress = listOf( - "Starting", - "Issuing cash", - "Generating transaction", - "Signing transaction", - "Finalising transaction", - "Broadcasting transaction to participants", - "Paying recipient", - "Generating anonymous identities", - "Generating transaction", - "Signing transaction", - "Finalising transaction", - "Requesting signature by notary service", - "Requesting signature by Notary service", - "Validating response from Notary service", - "Broadcasting transaction to participants", - "Done" - ) - - driver(DriverParameters(cordappsForAllNodes = FINANCE_CORDAPPS, startNodesInProcess = false, inMemoryDB = false)) { - fun startBankA(address: NetworkHostAndPort) = startNode(providedName = DUMMY_BANK_A_NAME, rpcUsers = listOf(demoUser), customOverrides = mapOf("rpcSettings.address" to address.toString())) - fun startProxy(addressPair: AddressPair) = RandomFailingProxy(serverPort = addressPair.proxyAddress.port, remotePort = addressPair.nodeAddress.port).start() - - val addresses = (1..2).map { getRandomAddressPair() } - currentAddressPair = addresses[0] - - proxy = startProxy(currentAddressPair) - val (bankA, bankB) = listOf( - startBankA(currentAddressPair.nodeAddress), - startNode(providedName = DUMMY_BANK_B_NAME, rpcUsers = listOf(demoUser)) - ).transpose().getOrThrow() - node = bankA - - val notary = defaultNotaryIdentity - val baseAmount = Amount.parseCurrency("0 USD") - val issuerRef = OpaqueBytes.of(0x01) - - var numDisconnects = 0 - var numReconnects = 0 - val maxStackOccurrences = AtomicInteger() - - val addressesForRpc = addresses.map { it.proxyAddress } - // DOCSTART rpcReconnectingRPC - val onReconnect = { - numReconnects++ - // We only expect to see a single reconnectOnError in the stack trace. Otherwise we're in danger of stack overflow recursion - maxStackOccurrences.set(max(maxStackOccurrences.get(), currentStackTrace().count { it.methodName == "reconnectOnError" })) - Unit - } - val reconnect = GracefulReconnect(onDisconnect = { numDisconnects++ }, onReconnect = onReconnect) - val config = CordaRPCClientConfiguration.DEFAULT.copy( - connectionRetryInterval = 1.seconds, - connectionRetryIntervalMultiplier = 1.0 - ) - val client = CordaRPCClient(addressesForRpc, configuration = config) - val bankAReconnectingRPCConnection = client.start(demoUser.username, demoUser.password, gracefulReconnect = reconnect) - val bankAReconnectingRpc = bankAReconnectingRPCConnection.proxy as ReconnectingCordaRPCOps - // DOCEND rpcReconnectingRPC - - // Observe the vault and collect the observations. - val vaultEvents = Collections.synchronizedList(mutableListOf>()) - // DOCSTART rpcReconnectingRPCVaultTracking - val vaultFeed = bankAReconnectingRpc.vaultTrackByWithPagingSpec( - Cash.State::class.java, - QueryCriteria.VaultQueryCriteria(), - PageSpecification(1, 1)) - val vaultSubscription = vaultFeed.updates.subscribe { update: Vault.Update -> - log.info("vault update produced ${update.produced.map { it.state.data.amount }} consumed ${update.consumed.map { it.ref }}") - vaultEvents.add(update) - } - // DOCEND rpcReconnectingRPCVaultTracking - - // Observe the stateMachine and collect the observations. - val stateMachineEvents = Collections.synchronizedList(mutableListOf()) - val stateMachineSubscription = bankAReconnectingRpc.stateMachinesFeed().updates.subscribe { update -> - log.info(update.toString()) - stateMachineEvents.add(update) - } - - // While the flows are running, randomly apply a different failure scenario. - val nrRestarts = AtomicInteger() - thread(name = "Node killer") { - while (true) { - if (flowsCountdownLatch.count == 0L) break - - // Let the node run for a random time interval. - nodeRunningTime().also { ms -> - log.info("Running node for ${ms / 1000} s.") - Thread.sleep(ms.toLong()) - } - - if (flowsCountdownLatch.count == 0L) break - when (Random().nextInt().rem(7).absoluteValue) { - 0 -> { - log.info("Forcefully killing node and proxy.") - (node as OutOfProcessImpl).onStopCallback() - (node as OutOfProcess).process.destroyForcibly() - proxy.stop() - node = startBankA(currentAddressPair.nodeAddress).get() - proxy.start() - } - 1 -> { - log.info("Forcefully killing node.") - (node as OutOfProcessImpl).onStopCallback() - (node as OutOfProcess).process.destroyForcibly() - node = startBankA(currentAddressPair.nodeAddress).get() - } - 2 -> { - log.info("Shutting down node.") - node.stop() - proxy.stop() - node = startBankA(currentAddressPair.nodeAddress).get() - proxy.start() - } - 3, 4 -> { - log.info("Killing proxy.") - proxy.stop() - Thread.sleep(Random().nextInt(5000).toLong()) - proxy.start() - } - 5 -> { - log.info("Dropping connection.") - proxy.failConnection() - } - 6 -> { - log.info("Performing failover to a different node") - node.stop() - proxy.stop() - currentAddressPair = (addresses - currentAddressPair).first() - node = startBankA(currentAddressPair.nodeAddress).get() - proxy = startProxy(currentAddressPair) - } - } - nrRestarts.incrementAndGet() - } - } - - // Start nrOfFlowsToRun and provide a logical retry function that checks the vault. - val flowProgressEvents = mutableMapOf>() - for (amount in (1..NUMBER_OF_FLOWS_TO_RUN)) { - // DOCSTART rpcReconnectingRPCFlowStarting - bankAReconnectingRpc.runFlowWithLogicalRetry( - runFlow = { rpc -> - log.info("Starting CashIssueAndPaymentFlow for $amount") - val flowHandle = rpc.startTrackedFlowDynamic( - CashIssueAndPaymentFlow::class.java, - baseAmount.plus(Amount.parseCurrency("$amount USD")), - issuerRef, - bankB.nodeInfo.legalIdentities.first(), - false, - notary - ) - val flowId = flowHandle.id - log.info("Started flow $amount with flowId: $flowId") - flowProgressEvents.addEvent(flowId, null) - - flowHandle.stepsTreeFeed?.updates?.notUsed() - flowHandle.stepsTreeIndexFeed?.updates?.notUsed() - // No reconnecting possible. - flowHandle.progress.subscribe( - { prog -> - flowProgressEvents.addEvent(flowId, prog) - log.info("Progress $flowId : $prog") - }, - { error -> - log.error("Error thrown in the flow progress observer", error) - }) - flowHandle.id - }, - hasFlowStarted = { rpc -> - // Query for a state that is the result of this flow. - val criteria = QueryCriteria.VaultCustomQueryCriteria(builder { CashSchemaV1.PersistentCashState::pennies.equal(amount.toLong() * 100) }, status = Vault.StateStatus.ALL) - val results = rpc.vaultQueryByCriteria(criteria, Cash.State::class.java) - log.info("$amount - Found states ${results.states}") - // The flow has completed if a state is found - results.states.isNotEmpty() - }, - onFlowConfirmed = { - flowsCountdownLatch.countDown() - log.info("Flow started for $amount. Remaining flows: ${flowsCountdownLatch.count}") - } - ) - // DOCEND rpcReconnectingRPCFlowStarting - - Thread.sleep(Random().nextInt(250).toLong()) - } - - log.info("Started all flows") - - // Wait until all flows have been started. - val flowsConfirmed = flowsCountdownLatch.await(10, TimeUnit.MINUTES) - - if (flowsConfirmed) { - log.info("Confirmed all flows have started.") - } else { - log.info("Timed out waiting for confirmation that all flows have started. Remaining flows: ${flowsCountdownLatch.count}") - } - - - // Wait for all events to come in and flows to finish. - Thread.sleep(4000) - - val nrFailures = nrRestarts.get() - log.info("Checking results after $nrFailures restarts.") - - // We should get one disconnect and one reconnect for each failure - assertThat(numDisconnects).isEqualTo(numReconnects) - assertThat(numReconnects).isLessThanOrEqualTo(nrFailures) - assertThat(maxStackOccurrences.get()).isLessThan(2) - - // Query the vault and check that states were created for all flows. - fun readCashStates() = bankAReconnectingRpc - .vaultQueryByWithPagingSpec(Cash.State::class.java, QueryCriteria.VaultQueryCriteria(status = Vault.StateStatus.CONSUMED), PageSpecification(1, 10000)) - .states - - var allCashStates = readCashStates() - var nrRetries = 0 - - // It might be necessary to wait more for all events to arrive when the node is slow. - while (allCashStates.size < NUMBER_OF_FLOWS_TO_RUN && nrRetries++ < 50) { - Thread.sleep(2000) - allCashStates = readCashStates() - } - - val allCash = allCashStates.map { it.state.data.amount.quantity }.toSet() - val missingCash = (1..NUMBER_OF_FLOWS_TO_RUN).filterNot { allCash.contains(it.toLong() * 100) } - log.info("Missing cash states: $missingCash") - - assertEquals(NUMBER_OF_FLOWS_TO_RUN, allCashStates.size, "Not all flows were executed successfully") - - // The progress status for each flow can only miss the last events, because the node might have been killed. - val missingProgressEvents = flowProgressEvents.filterValues { expectedProgress.subList(0, it.size) != it } - assertTrue(missingProgressEvents.isEmpty(), "The flow progress tracker is missing events: $missingProgressEvents") - - // DOCSTART missingVaultEvents - // Check that enough vault events were received. - // This check is fuzzy because events can go missing during node restarts. - // Ideally there should be nrOfFlowsToRun events receive but some might get lost for each restart. - assertThat(vaultEvents!!.size + nrFailures * 3).isGreaterThanOrEqualTo(NUMBER_OF_FLOWS_TO_RUN) - // DOCEND missingVaultEvents - - // Check that no flow was triggered twice. - val duplicates = allCashStates.groupBy { it.state.data.amount }.filterValues { it.size > 1 } - assertTrue(duplicates.isEmpty(), "${duplicates.size} flows were retried illegally.") - - log.info("State machine events seen: ${stateMachineEvents!!.size}") - // State machine events are very likely to get lost more often because they seem to be sent with a delay. - assertThat(stateMachineEvents.count { it is StateMachineUpdate.Added }).isGreaterThanOrEqualTo(NUMBER_OF_FLOWS_TO_RUN / 3) - assertThat(stateMachineEvents.count { it is StateMachineUpdate.Removed }).isGreaterThanOrEqualTo(NUMBER_OF_FLOWS_TO_RUN / 3) - - // Stop the observers. - vaultSubscription.unsubscribe() - stateMachineSubscription.unsubscribe() - bankAReconnectingRPCConnection.close() - } - - proxy.close() - } - - @Synchronized - fun MutableMap>.addEvent(id: StateMachineRunId, progress: String?): Boolean { - return getOrPut(id) { mutableListOf() }.let { if (progress != null) it.add(progress) else false } - } - private fun getRandomAddressPair() = AddressPair(getRandomAddress(), getRandomAddress()) - private fun getRandomAddress() = NetworkHostAndPort("localhost", portAllocator.nextPort()) - - data class AddressPair(val proxyAddress: NetworkHostAndPort, val nodeAddress: NetworkHostAndPort) -} From e6d5842a237930b0a400c3b46bbbb66a6077bc48 Mon Sep 17 00:00:00 2001 From: Yiftach Kaplan <67583323+yift-r3@users.noreply.github.com> Date: Fri, 17 Jul 2020 13:57:40 +0100 Subject: [PATCH 79/85] INFRA-482: Correct exception caught testing for node death (#6471) --- .../node/flows/FlowCheckpointVersionNodeStartupCheckTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/node/src/integration-test-slow/kotlin/net/corda/node/flows/FlowCheckpointVersionNodeStartupCheckTest.kt b/node/src/integration-test-slow/kotlin/net/corda/node/flows/FlowCheckpointVersionNodeStartupCheckTest.kt index d6abe718f1..7cc9b2643e 100644 --- a/node/src/integration-test-slow/kotlin/net/corda/node/flows/FlowCheckpointVersionNodeStartupCheckTest.kt +++ b/node/src/integration-test-slow/kotlin/net/corda/node/flows/FlowCheckpointVersionNodeStartupCheckTest.kt @@ -1,6 +1,7 @@ package net.corda.node.flows import co.paralleluniverse.fibers.Suspendable +import net.corda.core.CordaException import net.corda.core.flows.* import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party @@ -16,7 +17,6 @@ import net.corda.testing.driver.DriverDSL import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.NodeParameters import net.corda.testing.driver.driver -import net.corda.testing.node.internal.ListenProcessDeathException import net.corda.testing.node.internal.assertUncompletedCheckpoints import net.corda.testing.node.internal.enclosedCordapp import org.assertj.core.api.Assertions.assertThat @@ -77,7 +77,7 @@ class FlowCheckpointVersionNodeStartupCheckTest { private fun DriverDSL.assertBobFailsToStartWithLogMessage(logMessage: String) { assertUncompletedCheckpoints(BOB_NAME, 1) - assertFailsWith(ListenProcessDeathException::class) { + assertFailsWith(CordaException::class) { startNode(NodeParameters( providedName = BOB_NAME, customOverrides = mapOf("devMode" to false) From a500084d385fd43eb78dd00c4640b48d486fca6e Mon Sep 17 00:00:00 2001 From: Oliver Knowles Date: Mon, 20 Jul 2020 10:59:08 +0100 Subject: [PATCH 80/85] CORDA-3201 - Enforce separate key for notary identity (#6308) --- detekt-baseline.xml | 2 +- .../net/corda/node/internal/AbstractNode.kt | 23 ++- .../node/services/config/NodeConfiguration.kt | 2 +- .../registration/NetworkRegistrationHelper.kt | 153 ++++++++++++------ .../NetworkRegistrationHelperTest.kt | 116 +++++++++++-- samples/attachment-demo/build.gradle | 6 +- samples/bank-of-corda-demo/build.gradle | 6 +- samples/cordapp-configuration/build.gradle | 6 +- samples/irs-demo/cordapp/build.gradle | 10 +- samples/network-verifier/build.gradle | 6 +- samples/notary-demo/build.gradle | 11 +- samples/simm-valuation-demo/build.gradle | 6 +- samples/trader-demo/build.gradle | 6 +- 13 files changed, 263 insertions(+), 90 deletions(-) diff --git a/detekt-baseline.xml b/detekt-baseline.xml index 8e72535cd4..974e679f57 100644 --- a/detekt-baseline.xml +++ b/detekt-baseline.xml @@ -1435,7 +1435,7 @@ ThrowsCount:JarScanningCordappLoader.kt$JarScanningCordappLoader$private fun parseVersion(versionStr: String?, attributeName: String): Int ThrowsCount:LedgerDSLInterpreter.kt$Verifies$ fun failsWith(expectedMessage: String?): EnforceVerifyOrFail ThrowsCount:MockServices.kt$ fun <T : SerializeAsToken> createMockCordaService(serviceHub: MockServices, serviceConstructor: (AppServiceHub) -> T): T - ThrowsCount:NetworkRegistrationHelper.kt$NetworkRegistrationHelper$private fun validateCertificates(registeringPublicKey: PublicKey, certificates: List<X509Certificate>) + ThrowsCount:NetworkRegistrationHelper.kt$NetworkRegistrationHelper$private fun validateCertificates( registeringPublicKey: PublicKey, registeringLegalName: CordaX500Name, expectedCertRole: CertRole, certificates: List<X509Certificate> ) ThrowsCount:NodeInfoFilesCopier.kt$NodeInfoFilesCopier$private fun atomicCopy(source: Path, destination: Path) ThrowsCount:NodeVaultService.kt$NodeVaultService$@Throws(VaultQueryException::class) private fun <T : ContractState> _queryBy(criteria: QueryCriteria, paging_: PageSpecification, sorting: Sort, contractStateType: Class<out T>, skipPagingChecks: Boolean): Vault.Page<T> ThrowsCount:NodeVaultService.kt$NodeVaultService$private fun makeUpdates(batch: Iterable<CoreTransaction>, statesToRecord: StatesToRecord, previouslySeen: Boolean): List<Vault.Update<ContractState>> diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index c115fd45a4..59b6b4fca7 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -614,11 +614,22 @@ abstract class AbstractNode(val configuration: NodeConfiguration, val myNotaryIdentity = configuration.notary?.let { if (it.serviceLegalName != null) { - val (notaryIdentity, notaryIdentityKeyPair) = loadNotaryClusterIdentity(it.serviceLegalName) + val (notaryIdentity, notaryIdentityKeyPair) = loadNotaryServiceIdentity(it.serviceLegalName) keyPairs += notaryIdentityKeyPair notaryIdentity } else { - // In case of a single notary service myNotaryIdentity will be the node's single identity. + // The only case where the myNotaryIdentity will be the node's legal identity is for existing single notary services running + // an older version. Current single notary services (V4.6+) sign requests using a separate notary service identity so the + // notary identity will be different from the node's legal identity. + + // This check is here to ensure that a user does not accidentally/intentionally remove the serviceLegalName configuration + // parameter after a notary has been registered. If that was possible then notary would start and sign incoming requests + // with the node's legal identity key, corrupting the data. + check (!cryptoService.containsKey(DISTRIBUTED_NOTARY_KEY_ALIAS)) { + "The notary service key exists in the key store but no notary service legal name has been configured. " + + "Either include the relevant 'notary.serviceLegalName' configuration or validate this key is not necessary " + + "and remove from the key store." + } identity } } @@ -1057,8 +1068,12 @@ abstract class AbstractNode(val configuration: NodeConfiguration, } } - /** Loads pre-generated notary service cluster identity. */ - private fun loadNotaryClusterIdentity(serviceLegalName: CordaX500Name): Pair { + /** + * Loads notary service identity. In the case of the experimental RAFT and BFT notary clusters, this loads the pre-generated + * cluster identity that all worker nodes share. In the case of a simple single notary, this loads the notary service identity + * that is generated during initial registration and is used to sign notarisation requests. + * */ + private fun loadNotaryServiceIdentity(serviceLegalName: CordaX500Name): Pair { val privateKeyAlias = "$DISTRIBUTED_NOTARY_KEY_ALIAS" val compositeKeyAlias = "$DISTRIBUTED_NOTARY_COMPOSITE_KEY_ALIAS" diff --git a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt index f2dc3f16cb..a12989e169 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt @@ -151,7 +151,7 @@ fun NodeConfiguration.shouldInitCrashShell() = shouldStartLocalShell() || should data class NotaryConfig( /** Specifies whether the notary validates transactions or not. */ val validating: Boolean, - /** The legal name of cluster in case of a distributed notary service. */ + /** The legal name of the notary service identity. */ val serviceLegalName: CordaX500Name? = null, /** The name of the notary service class to load. */ val className: String? = null, diff --git a/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt b/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt index 45aa089f9e..8d2558ca8e 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt @@ -25,10 +25,10 @@ import org.bouncycastle.operator.ContentSigner import org.bouncycastle.util.io.pem.PemObject import java.io.IOException import java.io.StringWriter +import java.lang.IllegalStateException import java.net.ConnectException import java.net.URL import java.nio.file.Path -import java.security.KeyPair import java.security.PublicKey import java.security.cert.X509Certificate import java.time.Duration @@ -63,6 +63,7 @@ open class NetworkRegistrationHelper( private val requestIdStore = certificatesDirectory / "certificate-request-id.txt" protected val rootTrustStore: X509KeyStore protected val rootCert: X509Certificate + private val notaryServiceConfig: NotaryServiceConfig? = config.notaryServiceConfig init { require(networkRootTrustStorePath.exists()) { @@ -95,34 +96,70 @@ open class NetworkRegistrationHelper( return } + notaryServiceConfig?.let { validateNotaryServiceKeyAndCert(certStore, it.notaryServiceKeyAlias, it.notaryServiceLegalName) } + val tlsCrlIssuerCert = getTlsCrlIssuerCert() // We use SELF_SIGNED_PRIVATE_KEY as progress indicator so we just store a dummy key and cert. // When registration succeeds, this entry should be deleted. certStore.query { setPrivateKey(SELF_SIGNED_PRIVATE_KEY, AliasPrivateKey(SELF_SIGNED_PRIVATE_KEY), listOf(NOT_YET_REGISTERED_MARKER_KEYS_AND_CERTS.ECDSAR1_CERT), certificateStore.entryPassword) } - val nodeCaPublicKey = loadOrGenerateKeyPair() + val (entityPublicKey, receivedCertificates) = generateKeyPairAndCertificate(nodeCaKeyAlias, myLegalName, certRole, certStore) - val requestId = submitOrResumeCertificateSigningRequest(nodeCaPublicKey, cryptoService.getSigner(nodeCaKeyAlias)) - - val nodeCaCertificates = pollServerForCertificates(requestId) - validateCertificates(nodeCaPublicKey, nodeCaCertificates) - - certStore.setCertPathOnly(nodeCaKeyAlias, nodeCaCertificates) - certStore.value.internal.deleteEntry(SELF_SIGNED_PRIVATE_KEY) - certStore.value.save() - logProgress("Private key '$nodeCaKeyAlias' and its certificate-chain stored successfully.") - - onSuccess(nodeCaPublicKey, cryptoService.getSigner(nodeCaKeyAlias), nodeCaCertificates, tlsCrlIssuerCert?.subjectX500Principal?.toX500Name()) + onSuccess(entityPublicKey, cryptoService.getSigner(nodeCaKeyAlias), receivedCertificates, tlsCrlIssuerCert?.subjectX500Principal?.toX500Name()) // All done, clean up temp files. requestIdStore.deleteIfExists() } - private fun loadOrGenerateKeyPair(): PublicKey { - return if (cryptoService.containsKey(nodeCaKeyAlias)) { - cryptoService.getPublicKey(nodeCaKeyAlias)!! + private fun generateKeyPairAndCertificate(keyAlias: String, legalName: CordaX500Name, certificateRole: CertRole, certStore: CertificateStore): Pair> { + val entityPublicKey = loadOrGenerateKeyPair(keyAlias) + + val requestId = submitOrResumeCertificateSigningRequest(entityPublicKey, legalName, certificateRole, cryptoService.getSigner(keyAlias)) + + val receivedCertificates = pollServerForCertificates(requestId) + validateCertificates(entityPublicKey, legalName, certificateRole, receivedCertificates) + + certStore.setCertPathOnly(keyAlias, receivedCertificates) + certStore.value.internal.deleteEntry(SELF_SIGNED_PRIVATE_KEY) + certStore.value.save() + logProgress("Private key '$keyAlias' and its certificate-chain stored successfully.") + return Pair(entityPublicKey, receivedCertificates) + } + + /** + * Used when registering a notary to validate that the shared notary service key and certificate can be accessed. + * + * In the case that the notary service certificate and key is not available, a new key key is generated and a separate CSR is + * submitted to the Identity Manager. + * + * If this method successfully completes then the [cryptoService] will contain the notary service key and the [certStore] will contain + * the notary service certificate chain. + * + * @throws IllegalStateException If the notary service certificate already exists but the private key is not available. + */ + private fun validateNotaryServiceKeyAndCert(certStore: CertificateStore, notaryServiceKeyAlias: String, notaryServiceLegalName: CordaX500Name) { + if (certStore.contains(notaryServiceKeyAlias) && !cryptoService.containsKey(notaryServiceKeyAlias)) { + throw IllegalStateException("Notary service identity certificate exists but key pair missing. " + + "Please check no old certificates exist in the certificate store.") + } + + if (certStore.contains(notaryServiceKeyAlias)) { + logProgress("Notary service certificate already exists. Continuing with node registration...") + return + } + + logProgress("Generating notary service identity for $notaryServiceLegalName...") + generateKeyPairAndCertificate(notaryServiceKeyAlias, notaryServiceLegalName, CertRole.SERVICE_IDENTITY, certStore) + // The request id store is reused for the next step - registering the node identity. + // Therefore we can remove this to enable it to be reused. + requestIdStore.deleteIfExists() + } + + private fun loadOrGenerateKeyPair(keyAlias: String): PublicKey { + return if (cryptoService.containsKey(keyAlias)) { + cryptoService.getPublicKey(keyAlias)!! } else { - cryptoService.generateKeyPair(nodeCaKeyAlias, cryptoService.defaultTLSSignatureScheme()) + cryptoService.generateKeyPair(keyAlias, cryptoService.defaultTLSSignatureScheme()) } } @@ -137,26 +174,31 @@ open class NetworkRegistrationHelper( return tlsCrlIssuerCert } - private fun validateCertificates(registeringPublicKey: PublicKey, certificates: List) { - val nodeCACertificate = certificates.first() + private fun validateCertificates( + registeringPublicKey: PublicKey, + registeringLegalName: CordaX500Name, + expectedCertRole: CertRole, + certificates: List + ) { + val receivedCertificate = certificates.first() - val nodeCaSubject = try { - CordaX500Name.build(nodeCACertificate.subjectX500Principal) + val certificateSubject = try { + CordaX500Name.build(receivedCertificate.subjectX500Principal) } catch (e: IllegalArgumentException) { - throw CertificateRequestException("Received node CA cert has invalid subject name: ${e.message}") + throw CertificateRequestException("Received cert has invalid subject name: ${e.message}") } - if (nodeCaSubject != myLegalName) { - throw CertificateRequestException("Subject of received node CA cert doesn't match with node legal name: $nodeCaSubject") + if (certificateSubject != registeringLegalName) { + throw CertificateRequestException("Subject of received cert doesn't match with legal name: $certificateSubject") } - val nodeCaCertRole = try { - CertRole.extract(nodeCACertificate) + val receivedCertRole = try { + CertRole.extract(receivedCertificate) } catch (e: IllegalArgumentException) { - throw CertificateRequestException("Unable to extract cert role from received node CA cert: ${e.message}") + throw CertificateRequestException("Unable to extract cert role from received cert: ${e.message}") } - if (certRole != nodeCaCertRole) { - throw CertificateRequestException("Received certificate contains invalid cert role, expected '$certRole', got '$nodeCaCertRole'.") + if (expectedCertRole != receivedCertRole) { + throw CertificateRequestException("Received certificate contains invalid cert role, expected '$expectedCertRole', got '$receivedCertRole'.") } // Validate returned certificate is for the correct public key. @@ -169,22 +211,6 @@ open class NetworkRegistrationHelper( logProgress("Certificate signing request approved, storing private key with the certificate chain.") } - private fun CertificateStore.loadOrCreateKeyPair(alias: String, entryPassword: String = password): KeyPair { - // Create or load self signed keypair from the key store. - // We use the self sign certificate to store the key temporarily in the keystore while waiting for the request approval. - if (alias !in this) { - // NODE_CA should be TLS compatible due to the cert hierarchy structure. - val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val selfSignCert = X509Utilities.createSelfSignedCACertificate(myLegalName.x500Principal, keyPair) - // Save to the key store. - with(value) { - setPrivateKey(alias, keyPair.private, listOf(selfSignCert), keyPassword = entryPassword) - save() - } - } - return query { getCertificateAndKeyPair(alias, entryPassword) }.keyPair - } - /** * Poll Certificate Signing Server for approved certificate, * enter a slow polling loop if server return null. @@ -226,20 +252,27 @@ open class NetworkRegistrationHelper( * Submit Certificate Signing Request to Certificate signing service if request ID not found in file system. * New request ID will be stored in requestId.txt * @param publicKey public key for which we need a certificate. + * @param legalName legal name of the entity for which we need a certificate. + * @param certRole desired role of the entities certificate. * @param contentSigner the [ContentSigner] that will sign the CSR. * @return Request ID return from the server. */ - private fun submitOrResumeCertificateSigningRequest(publicKey: PublicKey, contentSigner: ContentSigner): String { + private fun submitOrResumeCertificateSigningRequest( + publicKey: PublicKey, + legalName: CordaX500Name, + certRole: CertRole, + contentSigner: ContentSigner + ): String { try { // Retrieve request id from file if exists, else post a request to server. return if (!requestIdStore.exists()) { - val request = X509Utilities.createCertificateSigningRequest(myLegalName.x500Principal, emailAddress, publicKey, contentSigner, certRole) + val request = X509Utilities.createCertificateSigningRequest(legalName.x500Principal, emailAddress, publicKey, contentSigner, certRole) val writer = StringWriter() JcaPEMWriter(writer).use { it.writeObject(PemObject("CERTIFICATE REQUEST", request.encoded)) } logProgress("Certificate signing request with the following information will be submitted to the Corda certificate signing server.") - logProgress("Legal Name: $myLegalName") + logProgress("Legal Name: $legalName") logProgress("Email: $emailAddress") logProgress("Public Key: $publicKey") logProgress("$writer") @@ -277,7 +310,8 @@ class NodeRegistrationConfiguration( val certificatesDirectory: Path, val emailAddress: String, val cryptoService: CryptoService, - val certificateStore: CertificateStore) { + val certificateStore: CertificateStore, + val notaryServiceConfig: NotaryServiceConfig? = null) { constructor(config: NodeConfiguration) : this( p2pSslOptions = config.p2pSslOptions, @@ -287,10 +321,29 @@ class NodeRegistrationConfiguration( certificatesDirectory = config.certificatesDirectory, emailAddress = config.emailAddress, cryptoService = BCCryptoService(config.myLegalName.x500Principal, config.signingCertificateStore), - certificateStore = config.signingCertificateStore.get(true) + certificateStore = config.signingCertificateStore.get(true), + notaryServiceConfig = config.notary?.let { + // Validation of the presence of the notary service legal name is only done here and not in the top level configuration + // file. This is to maintain backwards compatibility with older notaries using the legacy identity structure. Older + // notaries will be signing requests using the nodes legal identity key and therefore no separate notary service entity + // exists. Just having the validation here prevents any new notaries from being created with the legacy identity scheme + // but still allows drop in JAR replacements for old notaries. + requireNotNull(it.serviceLegalName) { + "The notary service legal name must be provided via the 'notary.serviceLegalName' configuration parameter" + } + require(it.serviceLegalName != config.myLegalName) { + "The notary service legal name must be different from the node legal name" + } + NotaryServiceConfig(X509Utilities.DISTRIBUTED_NOTARY_KEY_ALIAS, it.serviceLegalName!!) + } ) } +data class NotaryServiceConfig( + val notaryServiceKeyAlias: String, + val notaryServiceLegalName: CordaX500Name +) + class NodeRegistrationException( message: String?, cause: Throwable? diff --git a/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt b/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt index dde5082f6b..b17b437fad 100644 --- a/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt +++ b/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt @@ -28,6 +28,8 @@ import net.corda.testing.core.ALICE_NAME import net.corda.testing.internal.createDevIntermediateCaCertPath import net.corda.coretesting.internal.rigorousMock import net.corda.coretesting.internal.stubs.CertificateStoreStubs +import net.corda.node.services.config.NotaryConfig +import net.corda.testing.core.DUMMY_NOTARY_NAME import org.assertj.core.api.Assertions.* import org.bouncycastle.asn1.x509.GeneralName import org.bouncycastle.asn1.x509.GeneralSubtree @@ -37,6 +39,7 @@ import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest import org.junit.After import org.junit.Before import org.junit.Test +import java.lang.IllegalStateException import java.nio.file.Files import java.security.PublicKey import java.security.cert.CertPathValidatorException @@ -71,6 +74,7 @@ class NetworkRegistrationHelperTest { doReturn(null).whenever(it).tlsCertCrlDistPoint doReturn(null).whenever(it).tlsCertCrlIssuer doReturn(true).whenever(it).crlCheckSoftFail + doReturn(null).whenever(it).notary } } @@ -120,7 +124,7 @@ class NetworkRegistrationHelperTest { @Test(timeout=300_000) fun `missing truststore`() { - val nodeCaCertPath = createNodeCaCertPath() + val nodeCaCertPath = createCertPath() assertThatThrownBy { createFixedResponseRegistrationHelper(nodeCaCertPath) }.hasMessageContaining("This file must contain the root CA cert of your compatibility zone. Please contact your CZ operator.") @@ -128,7 +132,7 @@ class NetworkRegistrationHelperTest { @Test(timeout=300_000) fun `node CA with incorrect cert role`() { - val nodeCaCertPath = createNodeCaCertPath(type = CertificateType.TLS) + val nodeCaCertPath = createCertPath(type = CertificateType.TLS) saveNetworkTrustStore(CORDA_ROOT_CA to nodeCaCertPath.last()) val registrationHelper = createFixedResponseRegistrationHelper(nodeCaCertPath) assertThatExceptionOfType(CertificateRequestException::class.java) @@ -139,7 +143,7 @@ class NetworkRegistrationHelperTest { @Test(timeout=300_000) fun `node CA with incorrect subject`() { val invalidName = CordaX500Name("Foo", "MU", "GB") - val nodeCaCertPath = createNodeCaCertPath(legalName = invalidName) + val nodeCaCertPath = createCertPath(legalName = invalidName) saveNetworkTrustStore(CORDA_ROOT_CA to nodeCaCertPath.last()) val registrationHelper = createFixedResponseRegistrationHelper(nodeCaCertPath) assertThatExceptionOfType(CertificateRequestException::class.java) @@ -220,36 +224,118 @@ class NetworkRegistrationHelperTest { createRegistrationHelper(rootAndIntermediateCA = rootAndIntermediateCA).generateKeysAndRegister() } - private fun createNodeCaCertPath(type: CertificateType = CertificateType.NODE_CA, - legalName: CordaX500Name = nodeLegalName, - publicKey: PublicKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME).public, - rootAndIntermediateCA: Pair = createDevIntermediateCaCertPath()): List { + @Test(timeout=300_000) + fun `successful registration for notary node`() { + val notaryServiceLegalName = DUMMY_NOTARY_NAME + val notaryNodeConfig = createNotaryNodeConfiguration(notaryServiceLegalName = notaryServiceLegalName) + assertThat(notaryNodeConfig.notary).isNotNull + + val rootAndIntermediateCA = createDevIntermediateCaCertPath().also { + saveNetworkTrustStore(CORDA_ROOT_CA to it.first.certificate) + } + + // Mock out the registration service to ensure notary service registration is handled correctly + createRegistrationHelper(CertRole.NODE_CA, notaryNodeConfig) { + when { + it.subject == nodeLegalName.toX500Name() -> { + val certType = CertificateType.values().first { it.role == CertRole.NODE_CA } + createCertPath(rootAndIntermediateCA = rootAndIntermediateCA, publicKey = it.publicKey, type = certType) + } + it.subject == notaryServiceLegalName.toX500Name() -> { + val certType = CertificateType.values().first { it.role == CertRole.SERVICE_IDENTITY } + createCertPath(rootAndIntermediateCA = rootAndIntermediateCA, publicKey = it.publicKey, type = certType, legalName = notaryServiceLegalName) + } + else -> throw IllegalStateException("Unknown CSR") + } + }.generateKeysAndRegister() + + val nodeKeystore = config.signingCertificateStore.get() + + nodeKeystore.run { + assertFalse(contains(X509Utilities.CORDA_INTERMEDIATE_CA)) + assertFalse(contains(CORDA_ROOT_CA)) + assertFalse(contains(X509Utilities.CORDA_CLIENT_TLS)) + assertThat(CertRole.extract(this[X509Utilities.CORDA_CLIENT_CA])).isEqualTo(CertRole.NODE_CA) + assertThat(CertRole.extract(this[DISTRIBUTED_NOTARY_KEY_ALIAS])).isEqualTo(CertRole.SERVICE_IDENTITY) + } + } + + @Test(timeout=300_000) + fun `notary registration fails when no separate notary service identity configured`() { + val notaryNodeConfig = createNotaryNodeConfiguration(notaryServiceLegalName = null) + assertThat(notaryNodeConfig.notary).isNotNull + + assertThatThrownBy { + createRegistrationHelper(nodeConfig = notaryNodeConfig) + }.isInstanceOf(IllegalArgumentException::class.java) + .hasMessageContaining("notary service legal name must be provided") + } + + @Test(timeout=300_000) + fun `notary registration fails when notary service identity configured with same legal name as node`() { + val notaryNodeConfig = createNotaryNodeConfiguration(notaryServiceLegalName = config.myLegalName) + assertThat(notaryNodeConfig.notary).isNotNull + + assertThatThrownBy { + createRegistrationHelper(nodeConfig = notaryNodeConfig) + }.isInstanceOf(IllegalArgumentException::class.java) + .hasMessageContaining("notary service legal name must be different from the node") + } + + private fun createNotaryNodeConfiguration(notaryServiceLegalName: CordaX500Name?): NodeConfiguration { + return rigorousMock().also { + doReturn(config.baseDirectory).whenever(it).baseDirectory + doReturn(config.certificatesDirectory).whenever(it).certificatesDirectory + doReturn(CertificateStoreStubs.P2P.withCertificatesDirectory(config.certificatesDirectory)).whenever(it).p2pSslOptions + doReturn(CertificateStoreStubs.Signing.withCertificatesDirectory(config.certificatesDirectory)).whenever(it) + .signingCertificateStore + doReturn(nodeLegalName).whenever(it).myLegalName + doReturn("").whenever(it).emailAddress + doReturn(null).whenever(it).tlsCertCrlDistPoint + doReturn(null).whenever(it).tlsCertCrlIssuer + doReturn(true).whenever(it).crlCheckSoftFail + doReturn(NotaryConfig(validating = false, serviceLegalName = notaryServiceLegalName)).whenever(it).notary + } + } + + private fun createCertPath(type: CertificateType = CertificateType.NODE_CA, + legalName: CordaX500Name = nodeLegalName, + publicKey: PublicKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME).public, + rootAndIntermediateCA: Pair = createDevIntermediateCaCertPath()): List { val (rootCa, intermediateCa) = rootAndIntermediateCA val nameConstraints = if (type == CertificateType.NODE_CA) { NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, legalName.toX500Name()))), arrayOf()) } else { null } - val nodeCaCert = X509Utilities.createCertificate( + val cert = X509Utilities.createCertificate( type, intermediateCa.certificate, intermediateCa.keyPair, legalName.x500Principal, publicKey, nameConstraints = nameConstraints) - return listOf(nodeCaCert, intermediateCa.certificate, rootCa.certificate) + return listOf(cert, intermediateCa.certificate, rootCa.certificate) } private fun createFixedResponseRegistrationHelper(response: List, certRole: CertRole = CertRole.NODE_CA): NetworkRegistrationHelper { return createRegistrationHelper(certRole) { response } } - private fun createRegistrationHelper(certRole: CertRole = CertRole.NODE_CA, rootAndIntermediateCA: Pair = createDevIntermediateCaCertPath()) = createRegistrationHelper(certRole) { + private fun createRegistrationHelper( + certRole: CertRole = CertRole.NODE_CA, + rootAndIntermediateCA: Pair = createDevIntermediateCaCertPath(), + nodeConfig: NodeConfiguration = config + ) = createRegistrationHelper(certRole, nodeConfig) { val certType = CertificateType.values().first { it.role == certRole } - createNodeCaCertPath(rootAndIntermediateCA = rootAndIntermediateCA, publicKey = it.publicKey, type = certType) + createCertPath(rootAndIntermediateCA = rootAndIntermediateCA, publicKey = it.publicKey, type = certType) } - private fun createRegistrationHelper(certRole: CertRole = CertRole.NODE_CA, dynamicResponse: (JcaPKCS10CertificationRequest) -> List): NetworkRegistrationHelper { + private fun createRegistrationHelper( + certRole: CertRole = CertRole.NODE_CA, + nodeConfig: NodeConfiguration = config, + dynamicResponse: (JcaPKCS10CertificationRequest) -> List + ): NetworkRegistrationHelper { val certService = rigorousMock().also { val requests = mutableMapOf() doAnswer { @@ -265,11 +351,11 @@ class NetworkRegistrationHelperTest { } return when (certRole) { - CertRole.NODE_CA -> NodeRegistrationHelper(NodeRegistrationConfiguration(config), certService, NodeRegistrationOption(config.certificatesDirectory / networkRootTrustStoreFileName, networkRootTrustStorePassword)) + CertRole.NODE_CA -> NodeRegistrationHelper(NodeRegistrationConfiguration(nodeConfig), certService, NodeRegistrationOption(nodeConfig.certificatesDirectory / networkRootTrustStoreFileName, networkRootTrustStorePassword)) CertRole.SERVICE_IDENTITY -> NetworkRegistrationHelper( - NodeRegistrationConfiguration(config), + NodeRegistrationConfiguration(nodeConfig), certService, - config.certificatesDirectory / networkRootTrustStoreFileName, + nodeConfig.certificatesDirectory / networkRootTrustStoreFileName, networkRootTrustStorePassword, DISTRIBUTED_NOTARY_KEY_ALIAS, CertRole.SERVICE_IDENTITY) diff --git a/samples/attachment-demo/build.gradle b/samples/attachment-demo/build.gradle index e88c8fc431..43bb2c8cbd 100644 --- a/samples/attachment-demo/build.gradle +++ b/samples/attachment-demo/build.gradle @@ -92,8 +92,10 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar', nodeTask, cordapp project(':samples:attachment-demo:workflows') } node { - name "O=Notary Service,L=Zurich,C=CH" - notary = [validating: true] + name "O=Notary Node,L=Zurich,C=CH" + notary = [validating: true, + serviceLegalName: "O=Notary Service,L=Zurich,C=CH" + ] p2pPort 10002 cordapps = [] rpcUsers = ext.rpcUsers diff --git a/samples/bank-of-corda-demo/build.gradle b/samples/bank-of-corda-demo/build.gradle index e3ff1ad5c3..7749ba3cdd 100644 --- a/samples/bank-of-corda-demo/build.gradle +++ b/samples/bank-of-corda-demo/build.gradle @@ -50,8 +50,10 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar', nodeTask, cordapp project(':finance:contracts') } node { - name "O=Notary Service,L=Zurich,C=CH" - notary = [validating: true] + name "O=Notary Node,L=Zurich,C=CH" + notary = [validating: true, + serviceLegalName: "O=Notary Service,L=Zurich,C=CH" + ] p2pPort 10002 rpcSettings { address "localhost:10003" diff --git a/samples/cordapp-configuration/build.gradle b/samples/cordapp-configuration/build.gradle index 9d466ed986..723de77b5b 100644 --- a/samples/cordapp-configuration/build.gradle +++ b/samples/cordapp-configuration/build.gradle @@ -27,8 +27,10 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar', nodeTask, cordapp project(':samples:cordapp-configuration:workflows') } node { - name "O=Notary Service,L=Zurich,C=CH" - notary = [validating : true] + name "O=Notary Node,L=Zurich,C=CH" + notary = [validating : true, + serviceLegalName: "O=Notary Service,L=Zurich,C=CH" + ] p2pPort 10002 rpcSettings { port 10003 diff --git a/samples/irs-demo/cordapp/build.gradle b/samples/irs-demo/cordapp/build.gradle index 50474dd3e5..2fa1ca9f49 100644 --- a/samples/irs-demo/cordapp/build.gradle +++ b/samples/irs-demo/cordapp/build.gradle @@ -62,8 +62,10 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar', nodeTask]) cordapp project(':samples:irs-demo:cordapp:workflows-irs') } node { - name "O=Notary Service,L=Zurich,C=CH" - notary = [validating : true] + name "O=Notary Node,L=Zurich,C=CH" + notary = [validating : true, + serviceLegalName: "O=Notary Service,L=Zurich,C=CH" + ] p2pPort 10002 rpcSettings { address("localhost:10003") @@ -121,7 +123,9 @@ task prepareDockerNodes(type: net.corda.plugins.Dockerform, dependsOn: ['jar', n } node { name "O=Notary Service,L=Zurich,C=CH" - notary = [validating : true] + notary = [validating : true, + serviceLegalName: "O=Notary Service,L=Zurich,C=CH" + ] cordapps = ["${project(":finance").group}:contracts:$corda_release_version", "${project(":finance").group}:workflows:$corda_release_version"] rpcUsers = rpcUsersList useTestClock true diff --git a/samples/network-verifier/build.gradle b/samples/network-verifier/build.gradle index f7582c0069..92a2006e81 100644 --- a/samples/network-verifier/build.gradle +++ b/samples/network-verifier/build.gradle @@ -38,8 +38,10 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar', nodeTask]) cordapp project(':samples:network-verifier:workflows') } node { - name "O=Notary Service,L=Zurich,C=CH" - notary = [validating : false] + name "O=Notary Node,L=Zurich,C=CH" + notary = [validating : false, + serviceLegalName: "O=Notary Service,L=Zurich,C=CH" + ] p2pPort 10002 rpcSettings { port 10003 diff --git a/samples/notary-demo/build.gradle b/samples/notary-demo/build.gradle index a5a7a40117..ff2683b2a2 100644 --- a/samples/notary-demo/build.gradle +++ b/samples/notary-demo/build.gradle @@ -55,13 +55,15 @@ task deployNodesSingle(type: Cordform, dependsOn: ['jar', nodeTask, webTask]) { rpcUsers = [[user: "demou", password: "demop", permissions: ["ALL"]]] } node { - name "O=Notary Service,L=Zurich,C=CH" + name "O=Notary Node,L=Zurich,C=CH" p2pPort 10009 rpcSettings { address "localhost:10010" adminAddress "localhost:10110" } - notary = [validating: true] + notary = [validating: true, + serviceLegalName: "O=Notary Service,L=Zurich,C=CH" + ] } } @@ -85,7 +87,7 @@ task deployNodesCustom(type: Cordform, dependsOn: ['jar', nodeTask, webTask]) { rpcUsers = [[user: "demou", password: "demop", permissions: ["ALL"]]] } node { - name "O=Notary Service,L=Zurich,C=CH" + name "O=Notary Node,L=Zurich,C=CH" p2pPort 10009 rpcSettings { address "localhost:10010" @@ -93,7 +95,8 @@ task deployNodesCustom(type: Cordform, dependsOn: ['jar', nodeTask, webTask]) { } notary = [ validating: true, - className: "net.corda.notarydemo.MyCustomValidatingNotaryService" + className: "net.corda.notarydemo.MyCustomValidatingNotaryService", + serviceLegalName: "O=Notary Service,L=Zurich,C=CH" ] } } diff --git a/samples/simm-valuation-demo/build.gradle b/samples/simm-valuation-demo/build.gradle index f95a10716b..b0af7c3568 100644 --- a/samples/simm-valuation-demo/build.gradle +++ b/samples/simm-valuation-demo/build.gradle @@ -93,8 +93,10 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar', nodeTask, rpcUsers = [['username': "default", 'password': "default", 'permissions': [ 'ALL' ]]] } node { - name "O=Notary Service,L=Zurich,C=CH" - notary = [validating : true] + name "O=Notary Node,L=Zurich,C=CH" + notary = [validating : true, + serviceLegalName: "O=Notary Service,L=Zurich,C=CH" + ] p2pPort 10002 rpcSettings { address "localhost:10014" diff --git a/samples/trader-demo/build.gradle b/samples/trader-demo/build.gradle index 5eeea06740..5ac022fb30 100644 --- a/samples/trader-demo/build.gradle +++ b/samples/trader-demo/build.gradle @@ -83,8 +83,10 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar', nodeTask]) cordapp project(':samples:trader-demo:workflows-trader') } node { - name "O=Notary Service,L=Zurich,C=CH" - notary = [validating : true] + name "O=Notary Node,L=Zurich,C=CH" + notary = [validating : true, + serviceLegalName: "O=Notary Service,L=Zurich,C=CH" + ] p2pPort 10002 rpcSettings { address "localhost:10003" From e5a8351dc3564710ebf65da12ef730ac9a7ff4a5 Mon Sep 17 00:00:00 2001 From: Dimos Raptis Date: Mon, 20 Jul 2020 11:13:49 +0100 Subject: [PATCH 81/85] ENT-5437 - Add test for sendAll with multiple sessions from the same party (#6476) --- .../messaging/MessagingSendAllTest.kt | 72 +++++++++++++++++++ .../services/messaging/MessagingExecutor.kt | 4 +- 2 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 node/src/integration-test/kotlin/net/corda/node/services/messaging/MessagingSendAllTest.kt diff --git a/node/src/integration-test/kotlin/net/corda/node/services/messaging/MessagingSendAllTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/messaging/MessagingSendAllTest.kt new file mode 100644 index 0000000000..ad417530d5 --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/node/services/messaging/MessagingSendAllTest.kt @@ -0,0 +1,72 @@ +package net.corda.node.services.messaging + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.flows.Destination +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.FlowSession +import net.corda.core.flows.InitiatedBy +import net.corda.core.flows.InitiatingFlow +import net.corda.core.flows.StartableByRPC +import net.corda.core.internal.concurrent.transpose +import net.corda.core.messaging.startFlow +import net.corda.core.utilities.getOrThrow +import net.corda.core.utilities.unwrap +import net.corda.testing.core.ALICE_NAME +import net.corda.testing.core.BOB_NAME +import net.corda.testing.core.singleIdentity +import net.corda.testing.driver.DriverParameters +import net.corda.testing.driver.driver +import org.junit.Test +import kotlin.test.assertEquals + +class MessagingSendAllTest { + + @Test(timeout=300_000) + fun `flow can exchange messages with multiple sessions to the same party in parallel`() { + driver(DriverParameters(startNodesInProcess = true)) { + val (alice, bob) = listOf( + startNode(providedName = ALICE_NAME), + startNode(providedName = BOB_NAME) + ).transpose().getOrThrow() + + val bobIdentity = bob.nodeInfo.singleIdentity() + val messages = listOf( + bobIdentity to "hey bob 1", + bobIdentity to "hey bob 2" + ) + + alice.rpc.startFlow(::SenderFlow, messages).returnValue.getOrThrow() + } + } + + @StartableByRPC + @InitiatingFlow + class SenderFlow(private val parties: List>): FlowLogic() { + @Suspendable + override fun call(): String { + val messagesPerSession = parties.toList().map { (party, messageType) -> + val session = initiateFlow(party) + Pair(session, messageType) + }.toMap() + + sendAllMap(messagesPerSession) + val messages = receiveAll(String::class.java, messagesPerSession.keys.toList()) + + messages.map { it.unwrap { payload -> assertEquals("pong", payload) } } + + return "ok" + } + } + + @InitiatedBy(SenderFlow::class) + class RecipientFlow(private val otherPartySession: FlowSession): FlowLogic() { + @Suspendable + override fun call(): String { + otherPartySession.receive().unwrap { it } + otherPartySession.send("pong") + + return "ok" + } + } + +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/MessagingExecutor.kt b/node/src/main/kotlin/net/corda/node/services/messaging/MessagingExecutor.kt index eead9f5698..0734c958e1 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/MessagingExecutor.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/MessagingExecutor.kt @@ -54,8 +54,8 @@ class MessagingExecutor( } @Synchronized - fun sendAll(messages: Map) { - messages.forEach { recipients, message -> send(message, recipients) } + fun sendAll(messages: List>) { + messages.forEach { (recipients, message) -> send(message, recipients) } } @Synchronized From 3721b7c701b633d5884c559a240bc1748e5cf20b Mon Sep 17 00:00:00 2001 From: jakubbielawa Date: Mon, 20 Jul 2020 11:14:23 +0100 Subject: [PATCH 82/85] Decrease the ring buffer size (#6470) --- node/src/main/resources/log4j2.component.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/src/main/resources/log4j2.component.properties b/node/src/main/resources/log4j2.component.properties index 1b55982139..405c40b154 100644 --- a/node/src/main/resources/log4j2.component.properties +++ b/node/src/main/resources/log4j2.component.properties @@ -1,2 +1,2 @@ Log4jContextSelector=net.corda.node.utilities.logging.AsyncLoggerContextSelectorNoThreadLocal -AsyncLogger.RingBufferSize=262144 \ No newline at end of file +AsyncLogger.RingBufferSize=16384 \ No newline at end of file From d4444e520a0bca20c6c815c90dced7ad700f8608 Mon Sep 17 00:00:00 2001 From: Ryan Fowler Date: Mon, 20 Jul 2020 11:26:27 +0100 Subject: [PATCH 83/85] ENT-5140: Tighten network parameters checks (#6390) * ENT-5140: Tighten network parameters checks --- .../kotlin/net/corda/node/internal/NetworkParametersReader.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/node/src/main/kotlin/net/corda/node/internal/NetworkParametersReader.kt b/node/src/main/kotlin/net/corda/node/internal/NetworkParametersReader.kt index 0bab5cb88e..ce964ab97c 100644 --- a/node/src/main/kotlin/net/corda/node/internal/NetworkParametersReader.kt +++ b/node/src/main/kotlin/net/corda/node/internal/NetworkParametersReader.kt @@ -86,6 +86,7 @@ class NetworkParametersReader(private val trustRoot: X509Certificate, logger.info("No network-parameters file found. Expecting network parameters to be available from the network map.") networkMapClient ?: throw Error.NetworkMapNotConfigured() val signedParams = networkMapClient.getNetworkParameters(parametersHash) + signedParams.verifiedNetworkParametersCert(trustRoot) signedParams.serialize().open().copyTo(baseDirectory / NETWORK_PARAMS_FILE_NAME) return signedParams } From 5fef0726a2c54107a0cd6ea08753f571ae65039a Mon Sep 17 00:00:00 2001 From: Kyriakos Tharrouniatis Date: Mon, 20 Jul 2020 15:09:26 +0100 Subject: [PATCH 84/85] Add missing 'Suspendable' annotations to suspending methods (#6480) --- .../net/corda/node/services/statemachine/ActionExecutorImpl.kt | 2 ++ .../corda/node/services/statemachine/ActionFutureExecutor.kt | 3 +++ 2 files changed, 5 insertions(+) diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/ActionExecutorImpl.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/ActionExecutorImpl.kt index 2849dc03a1..435ae5d6f3 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/ActionExecutorImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/ActionExecutorImpl.kt @@ -73,6 +73,7 @@ internal class ActionExecutorImpl( if (action.uuid != null) services.vaultService.softLockRelease(action.uuid) } + @Suspendable private fun executeTrackTransaction(fiber: FlowFiber, action: Action.TrackTransaction) { actionFutureExecutor.awaitTransaction(fiber, action) } @@ -223,6 +224,7 @@ internal class ActionExecutorImpl( } @Suppress("TooGenericExceptionCaught") + @Suspendable private fun executeAsyncOperation(fiber: FlowFiber, action: Action.ExecuteAsyncOperation) { try { actionFutureExecutor.awaitAsyncOperation(fiber, action) diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/ActionFutureExecutor.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/ActionFutureExecutor.kt index dc5d2fc0b9..40ee343707 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/ActionFutureExecutor.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/ActionFutureExecutor.kt @@ -1,5 +1,6 @@ package net.corda.node.services.statemachine +import co.paralleluniverse.fibers.Suspendable import net.corda.core.internal.concurrent.thenMatch import net.corda.core.utilities.contextLogger import net.corda.core.utilities.debug @@ -45,6 +46,7 @@ internal class ActionFutureExecutor( * @param fiber The [FlowFiber] to resume after completing the async operation * @param action The [Action.ExecuteAsyncOperation] to create a future from */ + @Suspendable fun awaitAsyncOperation(fiber: FlowFiber, action: Action.ExecuteAsyncOperation) { cancelFutureIfRunning(fiber, action.currentState) val instance = fiber.instanceId @@ -63,6 +65,7 @@ internal class ActionFutureExecutor( * @param fiber The [FlowFiber] to resume after the committing the specified transaction * @param action [Action.TrackTransaction] contains the transaction hash to wait for */ + @Suspendable fun awaitTransaction(fiber: FlowFiber, action: Action.TrackTransaction) { cancelFutureIfRunning(fiber, action.currentState) val instance = fiber.instanceId From 1c48418904d20cd31d20766ef468022c01c387fd Mon Sep 17 00:00:00 2001 From: Viktor Kolomeyko Date: Tue, 21 Jul 2020 09:58:29 +0100 Subject: [PATCH 85/85] NOTICK: Remove `relaxedThoroughness` variable (#6483) It is: a. No longer works in Jenkins b. May result spawning 100s of process that kill Windows build. Evidence: https://ci02.dev.r3.com/blue/organizations/jenkins/Corda-Enterprise%2FCorda-ENT-MS-Win-Compatibility%2Fenterprise/detail/PR-3553/25/pipeline/ --- .../test/kotlin/net/corda/node/internal/AbstractNodeTests.kt | 4 +--- .../kotlin/net/corda/testing/common/internal/Thoroughness.kt | 3 --- 2 files changed, 1 insertion(+), 6 deletions(-) delete mode 100644 testing/test-common/src/main/kotlin/net/corda/testing/common/internal/Thoroughness.kt diff --git a/node/src/test/kotlin/net/corda/node/internal/AbstractNodeTests.kt b/node/src/test/kotlin/net/corda/node/internal/AbstractNodeTests.kt index e6655efd90..0fb0382234 100644 --- a/node/src/test/kotlin/net/corda/node/internal/AbstractNodeTests.kt +++ b/node/src/test/kotlin/net/corda/node/internal/AbstractNodeTests.kt @@ -6,7 +6,6 @@ import net.corda.core.internal.concurrent.transpose import net.corda.core.utilities.contextLogger import net.corda.core.utilities.getOrThrow import net.corda.nodeapi.internal.persistence.DatabaseConfig -import net.corda.testing.common.internal.relaxedThoroughness import net.corda.testing.internal.configureDatabase import net.corda.testing.node.internal.ProcessUtilities.startJavaProcess import org.junit.Rule @@ -43,8 +42,7 @@ class AbstractNodeTests { @Test(timeout=300_000) fun `H2 fix is applied`() { val pool = Executors.newFixedThreadPool(5) - val runs = if (relaxedThoroughness) 1 else 100 - (0 until runs).map { + (0 until 5).map { // Four "nodes" seems to be the magic number to reproduce the problem on CI: val urls = (0 until 4).map { freshURL() } // Haven't been able to reproduce in a warm JVM: diff --git a/testing/test-common/src/main/kotlin/net/corda/testing/common/internal/Thoroughness.kt b/testing/test-common/src/main/kotlin/net/corda/testing/common/internal/Thoroughness.kt deleted file mode 100644 index 298eaa5660..0000000000 --- a/testing/test-common/src/main/kotlin/net/corda/testing/common/internal/Thoroughness.kt +++ /dev/null @@ -1,3 +0,0 @@ -package net.corda.testing.common.internal - -val relaxedThoroughness = System.getenv("TEAMCITY_PROJECT_NAME") == "Pull Requests"