From 8f2e457f0d73454b823f90927158e58f5f8d2e0f Mon Sep 17 00:00:00 2001 From: Clinton Alexander Date: Thu, 14 Jun 2018 17:01:41 +0100 Subject: [PATCH 1/5] Removed some vestigal jolokia code from the webserver. --- webserver/build.gradle | 5 ----- 1 file changed, 5 deletions(-) diff --git a/webserver/build.gradle b/webserver/build.gradle index 3715cfaf67..93f3d85019 100644 --- a/webserver/build.gradle +++ b/webserver/build.gradle @@ -22,11 +22,6 @@ sourceSets { processResources { from file("$rootDir/config/dev/log4j2.xml") - from file("$rootDir/config/dev/jolokia-access.xml") -} - -processTestResources { - from file("$rootDir/config/test/jolokia-access.xml") } dependencies { From 1004c0f534f90d41008bc1201fc104bf0a630c45 Mon Sep 17 00:00:00 2001 From: Tudor Malene Date: Tue, 19 Jun 2018 14:57:03 +0100 Subject: [PATCH 2/5] CORDA-1407 Mark method as CordaInternal (#3402) --- core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt | 2 ++ 1 file changed, 2 insertions(+) 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 60c3465970..6844b95ca6 100644 --- a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt +++ b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt @@ -1,5 +1,6 @@ package net.corda.core.messaging +import net.corda.core.CordaInternal import net.corda.core.concurrent.CordaFuture import net.corda.core.context.InvocationContext import net.corda.core.contracts.ContractState @@ -199,6 +200,7 @@ interface CordaRPCOps : RPCOps { * * TODO This method should be removed once SGX work is finalised and the design of the corresponding API using [FilteredTransaction] can be started */ + @CordaInternal @Deprecated("This method is intended only for internal use and will be removed from the public API soon.") fun internalFindVerifiedTransaction(txnId: SecureHash): SignedTransaction? From d4b982b9fb65a7fe901a26c652a26da84d504723 Mon Sep 17 00:00:00 2001 From: Viktor Kolomeyko Date: Tue, 19 Jun 2018 15:29:35 +0100 Subject: [PATCH 3/5] ENT-2054: Logging improvements (#3397) --- core/src/main/kotlin/net/corda/core/flows/NotaryError.kt | 6 ++++-- .../core/serialization/NotaryExceptionSerializationTest.kt | 2 ++ .../main/kotlin/net/corda/finance/flows/CashPaymentFlow.kt | 4 ++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/flows/NotaryError.kt b/core/src/main/kotlin/net/corda/core/flows/NotaryError.kt index 281f3dfff6..4b082ad10f 100644 --- a/core/src/main/kotlin/net/corda/core/flows/NotaryError.kt +++ b/core/src/main/kotlin/net/corda/core/flows/NotaryError.kt @@ -15,7 +15,7 @@ class NotaryException( val error: NotaryError, /** Id of the transaction to be notarised. Can be _null_ if an error occurred before the id could be resolved. */ val txId: SecureHash? = null -) : FlowException("Unable to notarise transaction${txId ?: " "}: $error") +) : FlowException("Unable to notarise transaction ${txId ?: ""} : $error") /** Specifies the cause for notarisation request failure. */ @CordaSerializable @@ -27,7 +27,9 @@ sealed class NotaryError { /** Specifies which states have already been consumed in another transaction. */ val consumedStates: Map ) : NotaryError() { - override fun toString() = "One or more input states have been used in another transaction" + override fun toString() = "Conflict notarising transaction $txId. " + + "Input states have been used in another transactions, count: ${consumedStates.size}, " + + "content: ${consumedStates.asSequence().joinToString(limit = 5) { it.key.toString() + "->" + it.value }}" } /** Occurs when time specified in the [TimeWindow] command is outside the allowed tolerance. */ diff --git a/core/src/test/kotlin/net/corda/core/serialization/NotaryExceptionSerializationTest.kt b/core/src/test/kotlin/net/corda/core/serialization/NotaryExceptionSerializationTest.kt index b1cd4e7d4f..c0a9a642f6 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/NotaryExceptionSerializationTest.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/NotaryExceptionSerializationTest.kt @@ -10,6 +10,7 @@ import net.corda.testing.core.SerializationEnvironmentRule import org.junit.Rule import org.junit.Test import kotlin.test.assertEquals +import kotlin.test.assertTrue class NotaryExceptionSerializationTest { @Rule @@ -27,5 +28,6 @@ class NotaryExceptionSerializationTest { val instanceOnTheOtherSide = instance.serialize().bytes.deserialize() assertEquals(instance.error, instanceOnTheOtherSide.error) + assertTrue(instance.error.toString().contains("->")) } } \ No newline at end of file diff --git a/finance/src/main/kotlin/net/corda/finance/flows/CashPaymentFlow.kt b/finance/src/main/kotlin/net/corda/finance/flows/CashPaymentFlow.kt index f3afc3a107..b45d61067b 100644 --- a/finance/src/main/kotlin/net/corda/finance/flows/CashPaymentFlow.kt +++ b/finance/src/main/kotlin/net/corda/finance/flows/CashPaymentFlow.kt @@ -52,6 +52,7 @@ open class CashPaymentFlow( val anonymousRecipient = txIdentities[recipient] ?: recipient progressTracker.currentStep = GENERATING_TX val builder = TransactionBuilder(notary = null) + logger.info("Generating spend for: ${builder.lockId}") // TODO: Have some way of restricting this to states the caller controls val (spendTX, keysForSigning) = try { Cash.generateSpend(serviceHub, @@ -65,10 +66,13 @@ open class CashPaymentFlow( } progressTracker.currentStep = SIGNING_TX + logger.info("Signing transaction for: ${spendTX.lockId}") val tx = serviceHub.signInitialTransaction(spendTX, keysForSigning) progressTracker.currentStep = FINALISING_TX + logger.info("Finalising transaction for: ${tx.id}") val notarised = finaliseTx(tx, setOf(recipient), "Unable to notarise spend") + logger.info("Finalised transaction for: ${notarised.id}") return Result(notarised, anonymousRecipient) } From 41648d5a155574e1024c30642a2c6ce1810c5759 Mon Sep 17 00:00:00 2001 From: Andrius Dagys Date: Tue, 19 Jun 2018 16:42:55 +0100 Subject: [PATCH 4/5] CORDA-1645: Checkpoint before calling an idempotent sub-flow. (#3399) * CORDA-1645: Checkpoint before calling an idempotent sub-flow. When an idempotent sub-flow causes a flow restart, the flow will be replayed from the last checkpoint before the sub-flow invocation. However, the logic between the last checkpoint and the sub-flow invocation may contain side-effects which shouldn't be replayed. Thus we need to persist a checkpoint just before an idempotent sub-flow is invoked. --- .../net/corda/core/internal/FlowIORequest.kt | 7 ++ .../net/corda/core/internal/InternalUtils.kt | 6 ++ .../statemachine/FlowStateMachineImpl.kt | 16 ++++ .../statemachine/StaffedFlowHospital.kt | 4 +- .../transitions/StartedFlowTransition.kt | 25 ++--- .../statemachine/IdempotentFlowTests.kt | 93 +++++++++++++++++++ 6 files changed, 133 insertions(+), 18 deletions(-) create mode 100644 node/src/test/kotlin/net/corda/node/services/statemachine/IdempotentFlowTests.kt diff --git a/core/src/main/kotlin/net/corda/core/internal/FlowIORequest.kt b/core/src/main/kotlin/net/corda/core/internal/FlowIORequest.kt index 2c70cd54d1..257e7706c6 100644 --- a/core/src/main/kotlin/net/corda/core/internal/FlowIORequest.kt +++ b/core/src/main/kotlin/net/corda/core/internal/FlowIORequest.kt @@ -85,4 +85,11 @@ sealed class FlowIORequest { * Execute the specified [operation], suspend the flow until completion. */ data class ExecuteAsyncOperation(val operation: FlowAsyncOperation) : FlowIORequest() + + /** + * Indicates that no actual IO request occurred, and the flow should be resumed immediately. + * This is used for performing explicit checkpointing anywhere in a flow. + */ + // TODO: consider using an empty FlowAsyncOperation instead + object ForceCheckpoint : FlowIORequest() } diff --git a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt index a5fda08f08..9eb005d3ce 100644 --- a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt @@ -10,6 +10,7 @@ import net.corda.core.cordapp.Cordapp import net.corda.core.cordapp.CordappConfig import net.corda.core.cordapp.CordappContext import net.corda.core.crypto.* +import net.corda.core.flows.FlowLogic import net.corda.core.identity.CordaX500Name import net.corda.core.node.ServicesForResolution import net.corda.core.serialization.* @@ -515,6 +516,11 @@ fun createCordappContext(cordapp: Cordapp, attachmentId: SecureHash?, classLoade val PublicKey.hash: SecureHash get() = encoded.sha256() +/** Checks if this flow is an idempotent flow. */ +fun Class>.isIdempotentFlow(): Boolean { + return IdempotentFlow::class.java.isAssignableFrom(this) +} + /** * Extension method for providing a sumBy method that processes and returns a Long */ 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 8c6e23ab7f..d650a8c565 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 @@ -254,6 +254,7 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, @Suspendable override fun subFlow(subFlow: FlowLogic): R { + checkpointIfSubflowIdempotent(subFlow.javaClass) processEventImmediately( Event.EnterSubFlow(subFlow.javaClass, createSubFlowVersion( @@ -274,6 +275,21 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, } } + /** + * If the sub-flow is [IdempotentFlow] we need to perform a checkpoint to make sure any potentially side-effect + * generating logic between the last checkpoint and the sub-flow invocation does not get replayed if the + * flow restarts. + * + * We don't checkpoint if the current flow is [IdempotentFlow] as well. + */ + @Suspendable + private fun checkpointIfSubflowIdempotent(subFlow: Class>) { + val currentFlow = snapshot().checkpoint.subFlowStack.last().flowClass + if (!currentFlow.isIdempotentFlow() && subFlow.isIdempotentFlow()) { + suspend(FlowIORequest.ForceCheckpoint, false) + } + } + @Suspendable override fun initiateFlow(party: Party): FlowSession { val resume = processEventImmediately( 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 0a2e70ce6f..2779cee916 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 @@ -206,8 +206,8 @@ class StaffedFlowHospital { if (history.notDischargedForTheSameThingMoreThan(newError.maxRetries, this)) { return Diagnosis.DISCHARGE } else { - val errorMsg = "Maximum number of retries reached for flow ${flowFiber.snapshot().flowLogic.javaClass}." + - "If the flow involves notarising a transaction, this usually means that the notary is being overloaded and" + + val errorMsg = "Maximum number of retries reached for flow ${flowFiber.snapshot().flowLogic.javaClass}. " + + "If the flow involves notarising a transaction, this usually means that the notary is being overloaded and " + "unable to service requests fast enough. Please try again later." newError.setMessage(errorMsg) log.warn(errorMsg) 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 41bfd2dfee..7dd43cb299 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 @@ -6,22 +6,7 @@ import net.corda.core.flows.UnexpectedFlowEndException import net.corda.core.internal.FlowIORequest import net.corda.core.serialization.SerializedBytes import net.corda.core.utilities.toNonEmptySet -import net.corda.node.services.statemachine.Action -import net.corda.node.services.statemachine.Checkpoint -import net.corda.node.services.statemachine.DataSessionMessage -import net.corda.node.services.statemachine.DeduplicationId -import net.corda.node.services.statemachine.ExistingSessionMessage -import net.corda.node.services.statemachine.FlowError -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.SessionMap -import net.corda.node.services.statemachine.SessionState -import net.corda.node.services.statemachine.StateMachineState -import net.corda.node.services.statemachine.SubFlow +import net.corda.node.services.statemachine.* /** * This transition describes what should happen with a specific [FlowIORequest]. Note that at this time the request @@ -55,6 +40,7 @@ class StartedFlowTransition( is FlowIORequest.GetFlowInfo -> getFlowInfoTransition(flowIORequest) is FlowIORequest.WaitForSessionConfirmations -> waitForSessionConfirmationsTransition() is FlowIORequest.ExecuteAsyncOperation<*> -> executeAsyncOperation(flowIORequest) + FlowIORequest.ForceCheckpoint -> executeForceCheckpoint() } } @@ -400,6 +386,9 @@ class StartedFlowTransition( is FlowIORequest.ExecuteAsyncOperation<*> -> { emptyList() } + FlowIORequest.ForceCheckpoint -> { + emptyList() + } } } @@ -426,4 +415,8 @@ class StartedFlowTransition( FlowContinuation.ProcessEvents } } + + private fun executeForceCheckpoint(): TransitionResult { + return builder { resumeFlowLogic(Unit) } + } } diff --git a/node/src/test/kotlin/net/corda/node/services/statemachine/IdempotentFlowTests.kt b/node/src/test/kotlin/net/corda/node/services/statemachine/IdempotentFlowTests.kt new file mode 100644 index 0000000000..459a67b1a2 --- /dev/null +++ b/node/src/test/kotlin/net/corda/node/services/statemachine/IdempotentFlowTests.kt @@ -0,0 +1,93 @@ +package net.corda.node.services.statemachine + +import co.paralleluniverse.fibers.Suspendable +import com.nhaarman.mockito_kotlin.doReturn +import com.nhaarman.mockito_kotlin.whenever +import net.corda.core.crypto.SecureHash +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.InitiatingFlow +import net.corda.core.identity.CordaX500Name +import net.corda.core.internal.IdempotentFlow +import net.corda.core.internal.TimedFlow +import net.corda.core.internal.packageName +import net.corda.core.utilities.seconds +import net.corda.node.internal.StartedNode +import net.corda.node.services.config.FlowTimeoutConfiguration +import net.corda.node.services.config.NodeConfiguration +import net.corda.testing.node.internal.InternalMockNetwork +import net.corda.testing.node.internal.InternalMockNodeParameters +import net.corda.testing.node.internal.startFlow +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicInteger +import kotlin.test.assertEquals + +class IdempotentFlowTests { + private lateinit var mockNet: InternalMockNetwork + private lateinit var nodeA: StartedNode + private lateinit var nodeB: StartedNode + + companion object { + val executionCounter = AtomicInteger(0) + val subFlowExecutionCounter = AtomicInteger(0) + val suspendedOnce = AtomicBoolean(false) + } + + @Before + fun start() { + mockNet = InternalMockNetwork(threadPerNode = true, cordappPackages = listOf(this.javaClass.packageName)) + nodeA = mockNet.createNode(InternalMockNodeParameters( + legalName = CordaX500Name("Alice", "AliceCorp", "GB"), + configOverrides = { + conf: NodeConfiguration -> + val retryConfig = FlowTimeoutConfiguration(1.seconds, 3, 1.0) + doReturn(retryConfig).whenever(conf).flowTimeout + } + )) + nodeB = mockNet.createNode() + mockNet.startNodes() + executionCounter.set(0) + subFlowExecutionCounter.set(0) + suspendedOnce.set(false) + } + + @After + fun cleanUp() { + mockNet.stopNodes() + } + + @Test + fun `restarting idempotent flow does not replay any part of its parent flow`() { + nodeA.services.startFlow(SideEffectFlow()).resultFuture.get() + assertEquals(1, executionCounter.get()) + assertEquals(2, subFlowExecutionCounter.get()) + } + + @InitiatingFlow + private class SideEffectFlow : FlowLogic() { + @Suspendable + override fun call() { + executionCounter.incrementAndGet() // This shouldn't be replayed when the TimedSubFlow restarts. + subFlow(TimedSubflow()) // Checkpoint should be taken before invoking the sub-flow. + } + } + + private class TimedSubflow : FlowLogic(), TimedFlow { + @Suspendable + override fun call() { + subFlowExecutionCounter.incrementAndGet() // No checkpoint should be taken before invoking IdempotentSubFlow, + // so this should be replayed when TimedSubFlow restarts. + subFlow(IdempotentSubFlow()) // Checkpoint shouldn't be taken before invoking the sub-flow. + } + } + + private class IdempotentSubFlow : FlowLogic(), IdempotentFlow { + @Suspendable + override fun call() { + if (!IdempotentFlowTests.suspendedOnce.getAndSet(true)) + waitForLedgerCommit(SecureHash.zeroHash) + } + } +} \ No newline at end of file From af93ff8d1c44518a8effad2c3d0e45926a3dcfc7 Mon Sep 17 00:00:00 2001 From: Michele Sollecito Date: Tue, 19 Jun 2018 16:43:46 +0100 Subject: [PATCH 5/5] [CORDA-1638]: Node crashes in --initial-registration polling mode if doorman returns a transient HTTP error (fix). (#3403) --- docs/source/changelog.rst | 3 ++ .../net/corda/node/internal/NodeStartup.kt | 4 ++ .../HTTPNetworkRegistrationService.kt | 5 +- .../registration/NetworkRegistrationHelper.kt | 46 ++++++++++++++++--- 4 files changed, 50 insertions(+), 8 deletions(-) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index defe4df2fa..f13d1b99f0 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -7,6 +7,9 @@ release, see :doc:`upgrade-notes`. Unreleased ========== +* Introduced a grace period before the initial node registration fails if the node cannot connect to the Doorman. + It retries 10 times with a 1 minute interval in between each try. At the moment this is not configurable. + * Added a ``FlowMonitor`` to log information about flows that have been waiting for IO more than a configurable threshold. * H2 database changes: diff --git a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt index 1c0dd7bf77..c2321f9d7b 100644 --- a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt +++ b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt @@ -25,6 +25,7 @@ import net.corda.node.services.config.shouldStartSSHDaemon import net.corda.node.services.transactions.bftSMaRtSerialFilter import net.corda.node.utilities.registration.HTTPNetworkRegistrationService import net.corda.node.utilities.registration.NodeRegistrationHelper +import net.corda.node.utilities.registration.UnableToRegisterNodeWithDoormanException import net.corda.nodeapi.internal.addShutdownHook import net.corda.nodeapi.internal.config.UnknownConfigurationKeysException import net.corda.nodeapi.internal.persistence.CouldNotCreateDataSourceException @@ -121,6 +122,9 @@ open class NodeStartup(val args: Array) { return true } logStartupInfo(versionInfo, cmdlineOptions, conf) + } catch (e: UnableToRegisterNodeWithDoormanException) { + logger.warn("Node registration service is unavailable. Perhaps try to perform the initial registration again after a while.") + return false } catch (e: Exception) { logger.error("Exception during node registration", e) return false diff --git a/node/src/main/kotlin/net/corda/node/utilities/registration/HTTPNetworkRegistrationService.kt b/node/src/main/kotlin/net/corda/node/utilities/registration/HTTPNetworkRegistrationService.kt index 7af94f617d..14a37ab0a0 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/registration/HTTPNetworkRegistrationService.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/registration/HTTPNetworkRegistrationService.kt @@ -16,6 +16,7 @@ import java.net.URL import java.security.cert.X509Certificate import java.util.* import java.util.zip.ZipInputStream +import javax.naming.ServiceUnavailableException class HTTPNetworkRegistrationService(compatibilityZoneURL: URL) : NetworkRegistrationService { private val registrationURL = URL("$compatibilityZoneURL/certificate") @@ -23,6 +24,7 @@ class HTTPNetworkRegistrationService(compatibilityZoneURL: URL) : NetworkRegistr companion object { // TODO: Propagate version information from gradle const val CLIENT_VERSION = "1.0" + private val TRANSIENT_ERROR_STATUS_CODES = setOf(HTTP_BAD_GATEWAY, HTTP_UNAVAILABLE, HTTP_GATEWAY_TIMEOUT) } @Throws(CertificateRequestException::class) @@ -45,7 +47,8 @@ class HTTPNetworkRegistrationService(compatibilityZoneURL: URL) : NetworkRegistr } HTTP_NO_CONTENT -> CertificateResponse(pollInterval, null) HTTP_UNAUTHORIZED -> throw CertificateRequestException("Certificate signing request has been rejected: ${conn.errorMessage}") - else -> throw IOException("Response Code ${conn.responseCode}: ${conn.errorMessage}") + in TRANSIENT_ERROR_STATUS_CODES -> throw ServiceUnavailableException("Could not connect with Doorman. Http response status code was ${conn.responseCode}.") + else -> throw IOException("Error while connecting to the Doorman. Http response status code was ${conn.responseCode}.") } } 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 302afeee46..6a804bfc8f 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 @@ -16,12 +16,15 @@ import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_ROOT_CA import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.openssl.jcajce.JcaPEMWriter import org.bouncycastle.util.io.pem.PemObject +import java.io.IOException import java.io.StringWriter import java.nio.file.Path import java.security.KeyPair import java.security.KeyStore import java.security.PublicKey import java.security.cert.X509Certificate +import java.time.Duration +import javax.naming.ServiceUnavailableException /** * Helper for managing the node registration process, which checks for any existing certificates and requests them if @@ -35,7 +38,8 @@ open class NetworkRegistrationHelper(private val config: SSLConfiguration, private val networkRootTrustStorePath: Path, networkRootTrustStorePassword: String, private val keyAlias: String, - private val certRole: CertRole) { + private val certRole: CertRole, + private val nextIdleDuration: (Duration?) -> Duration? = FixedPeriodLimitedRetrialStrategy(10, Duration.ofMinutes(1))) { companion object { const val SELF_SIGNED_PRIVATE_KEY = "Self Signed Private Key" @@ -160,12 +164,22 @@ open class NetworkRegistrationHelper(private val config: SSLConfiguration, private fun pollServerForCertificates(requestId: String): List { println("Start polling server for certificate signing approval.") // Poll server to download the signed certificate once request has been approved. + var idlePeriodDuration: Duration? = null while (true) { - val (pollInterval, certificates) = certService.retrieveCertificates(requestId) - if (certificates != null) { - return certificates + try { + val (pollInterval, certificates) = certService.retrieveCertificates(requestId) + if (certificates != null) { + return certificates + } + Thread.sleep(pollInterval.toMillis()) + } catch (e: ServiceUnavailableException) { + idlePeriodDuration = nextIdleDuration(idlePeriodDuration) + if (idlePeriodDuration != null) { + Thread.sleep(idlePeriodDuration.toMillis()) + } else { + throw UnableToRegisterNodeWithDoormanException() + } } - Thread.sleep(pollInterval.toMillis()) } } @@ -208,7 +222,9 @@ open class NetworkRegistrationHelper(private val config: SSLConfiguration, protected open fun onSuccess(nodeCAKeyPair: KeyPair, certificates: List) {} } -class NodeRegistrationHelper(private val config: NodeConfiguration, certService: NetworkRegistrationService, regConfig: NodeRegistrationOption) : +class UnableToRegisterNodeWithDoormanException : IOException() + +class NodeRegistrationHelper(private val config: NodeConfiguration, certService: NetworkRegistrationService, regConfig: NodeRegistrationOption, computeNextIdleDoormanConnectionPollInterval: (Duration?) -> Duration? = FixedPeriodLimitedRetrialStrategy(10, Duration.ofMinutes(1))) : NetworkRegistrationHelper(config, config.myLegalName, config.emailAddress, @@ -216,7 +232,8 @@ class NodeRegistrationHelper(private val config: NodeConfiguration, certService: regConfig.networkRootTrustStorePath, regConfig.networkRootTrustStorePassword, CORDA_CLIENT_CA, - CertRole.NODE_CA) { + CertRole.NODE_CA, + computeNextIdleDoormanConnectionPollInterval) { companion object { val logger = contextLogger() @@ -255,3 +272,18 @@ class NodeRegistrationHelper(private val config: NodeConfiguration, certService: println("Node trust store stored in ${config.trustStoreFile}.") } } + +private class FixedPeriodLimitedRetrialStrategy(times: Int, private val period: Duration) : (Duration?) -> Duration? { + + init { + require(times > 0) + } + + private var counter = times + + override fun invoke(@Suppress("UNUSED_PARAMETER") previousPeriod: Duration?): Duration? { + synchronized(this) { + return if (counter-- > 0) period else null + } + } +} \ No newline at end of file