mirror of
https://github.com/corda/corda.git
synced 2025-06-17 14:48:16 +00:00
[CORDA-1941]: Server-side draining node shutdown. (#3909)
This commit is contained in:
committed by
GitHub
parent
0b4d1c0f35
commit
5113f4c8c1
@ -1,15 +1,20 @@
|
||||
package net.corda.node.modes.draining
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.client.rpc.internal.drainAndShutdown
|
||||
import net.corda.core.flows.*
|
||||
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.identity.Party
|
||||
import net.corda.core.internal.concurrent.map
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.unwrap
|
||||
import net.corda.node.services.Permissions
|
||||
import net.corda.nodeapi.internal.hasCancelledDrainingShutdown
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.BOB_NAME
|
||||
import net.corda.testing.core.singleIdentity
|
||||
@ -18,10 +23,12 @@ import net.corda.testing.driver.PortAllocation
|
||||
import net.corda.testing.driver.driver
|
||||
import net.corda.testing.internal.chooseIdentity
|
||||
import net.corda.testing.node.User
|
||||
import net.corda.testing.node.internal.waitForShutdown
|
||||
import org.assertj.core.api.AssertionsForInterfaceTypes.assertThat
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import rx.Observable
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.ScheduledExecutorService
|
||||
@ -80,24 +87,74 @@ class P2PFlowsDrainingModeTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clean shutdown by draining`() {
|
||||
driver(DriverParameters(startNodesInProcess = true, portAllocation = portAllocation, notarySpecs = emptyList())) {
|
||||
fun `terminate node waiting for pending flows`() {
|
||||
|
||||
driver(DriverParameters(portAllocation = portAllocation, notarySpecs = emptyList())) {
|
||||
|
||||
val nodeA = startNode(providedName = ALICE_NAME, rpcUsers = users).getOrThrow()
|
||||
val nodeB = startNode(providedName = BOB_NAME, rpcUsers = users).getOrThrow()
|
||||
var successful = false
|
||||
val latch = CountDownLatch(1)
|
||||
|
||||
nodeB.rpc.setFlowsDrainingModeEnabled(true)
|
||||
IntRange(1, 10).forEach { nodeA.rpc.startFlow(::InitiateSessionFlow, nodeB.nodeInfo.chooseIdentity()) }
|
||||
|
||||
nodeA.rpc.drainAndShutdown()
|
||||
.doOnError { error ->
|
||||
error.printStackTrace()
|
||||
successful = false
|
||||
}
|
||||
.doOnCompleted { successful = true }
|
||||
.doAfterTerminate { latch.countDown() }
|
||||
.subscribe()
|
||||
nodeA.waitForShutdown().doOnError(Throwable::printStackTrace).doOnError { successful = false }.doOnCompleted { successful = true }.doAfterTerminate(latch::countDown).subscribe()
|
||||
|
||||
nodeA.rpc.terminate(true)
|
||||
nodeB.rpc.setFlowsDrainingModeEnabled(false)
|
||||
|
||||
latch.await()
|
||||
|
||||
assertThat(successful).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `terminate resets persistent draining mode property when waiting for pending flows`() {
|
||||
|
||||
driver(DriverParameters(portAllocation = portAllocation, notarySpecs = emptyList())) {
|
||||
|
||||
val nodeA = startNode(providedName = ALICE_NAME, rpcUsers = users).getOrThrow()
|
||||
var successful = false
|
||||
val latch = CountDownLatch(1)
|
||||
|
||||
// This would not be needed, as `terminate(true)` sets draining mode anyway, but it's here to ensure that it removes the persistent value anyway.
|
||||
nodeA.rpc.setFlowsDrainingModeEnabled(true)
|
||||
nodeA.rpc.waitForShutdown().doOnError(Throwable::printStackTrace).doOnError { successful = false }.doOnCompleted(nodeA::stop).doOnCompleted {
|
||||
val nodeARestarted = startNode(providedName = ALICE_NAME, rpcUsers = users).getOrThrow()
|
||||
successful = !nodeARestarted.rpc.isFlowsDrainingModeEnabled()
|
||||
}.doAfterTerminate(latch::countDown).subscribe()
|
||||
|
||||
nodeA.rpc.terminate(true)
|
||||
|
||||
latch.await()
|
||||
|
||||
assertThat(successful).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `disabling draining mode cancels draining shutdown`() {
|
||||
|
||||
driver(DriverParameters(portAllocation = portAllocation, notarySpecs = emptyList())) {
|
||||
|
||||
val nodeA = startNode(providedName = ALICE_NAME, rpcUsers = users).getOrThrow()
|
||||
val nodeB = startNode(providedName = BOB_NAME, rpcUsers = users).getOrThrow()
|
||||
var successful = false
|
||||
val latch = CountDownLatch(1)
|
||||
|
||||
nodeB.rpc.setFlowsDrainingModeEnabled(true)
|
||||
IntRange(1, 10).forEach { nodeA.rpc.startFlow(::InitiateSessionFlow, nodeB.nodeInfo.chooseIdentity()) }
|
||||
|
||||
nodeA.waitForShutdown().doOnError(Throwable::printStackTrace).doAfterTerminate { successful = false }.doAfterTerminate(latch::countDown).subscribe()
|
||||
|
||||
nodeA.rpc.terminate(true)
|
||||
nodeA.rpc.hasCancelledDrainingShutdown().doOnError(Throwable::printStackTrace).doOnError { successful = false }.doOnCompleted { successful = true }.doAfterTerminate(latch::countDown).subscribe()
|
||||
|
||||
nodeA.rpc.setFlowsDrainingModeEnabled(false)
|
||||
nodeB.rpc.setFlowsDrainingModeEnabled(false)
|
||||
|
||||
latch.await()
|
||||
|
||||
assertThat(successful).isTrue()
|
||||
|
@ -238,7 +238,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
|
||||
/** The implementation of the [CordaRPCOps] interface used by this node. */
|
||||
open fun makeRPCOps(): CordaRPCOps {
|
||||
val ops: CordaRPCOps = CordaRPCOpsImpl(services, smm, flowStarter) { shutdownExecutor.submit { stop() } }
|
||||
val ops: CordaRPCOps = CordaRPCOpsImpl(services, smm, flowStarter) { shutdownExecutor.submit { stop() } }.also { it.closeOnStop() }
|
||||
val proxies = mutableListOf<(CordaRPCOps) -> CordaRPCOps>()
|
||||
// Mind that order is relevant here.
|
||||
proxies += ::AuthenticatedRpcOpsProxy
|
||||
|
@ -28,17 +28,21 @@ import net.corda.core.node.services.vault.*
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.node.services.api.FlowStarter
|
||||
import net.corda.node.services.api.ServiceHubInternal
|
||||
import net.corda.node.services.messaging.context
|
||||
import net.corda.node.services.statemachine.StateMachineManager
|
||||
import net.corda.nodeapi.exceptions.NonRpcFlowException
|
||||
import net.corda.nodeapi.exceptions.RejectedCommandException
|
||||
import net.corda.nodeapi.internal.pendingFlowsCount
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
import java.io.InputStream
|
||||
import java.net.ConnectException
|
||||
import java.security.PublicKey
|
||||
import java.time.Instant
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
/**
|
||||
* Server side implementations of RPCs available to MQ based client tools. Execution takes place on the server
|
||||
@ -49,7 +53,24 @@ internal class CordaRPCOpsImpl(
|
||||
private val smm: StateMachineManager,
|
||||
private val flowStarter: FlowStarter,
|
||||
private val shutdownNode: () -> Unit
|
||||
) : CordaRPCOps {
|
||||
) : CordaRPCOps, AutoCloseable {
|
||||
|
||||
private companion object {
|
||||
private val logger = loggerFor<CordaRPCOpsImpl>()
|
||||
}
|
||||
|
||||
private val drainingShutdownHook = AtomicReference<Subscription?>()
|
||||
|
||||
init {
|
||||
services.nodeProperties.flowsDrainingMode.values.filter { it.isDisabled() }.subscribe({
|
||||
cancelDrainingShutdownHook()
|
||||
}, {
|
||||
// Nothing to do in case of errors here.
|
||||
})
|
||||
}
|
||||
|
||||
private fun Pair<Boolean, Boolean>.isDisabled(): Boolean = first && !second
|
||||
|
||||
/**
|
||||
* Returns the RPC protocol version, which is the same the node's platform Version. Exists since version 1 so guaranteed
|
||||
* to be present.
|
||||
@ -222,7 +243,7 @@ internal class CordaRPCOpsImpl(
|
||||
return services.networkMapCache.getNodeByLegalIdentity(party)
|
||||
}
|
||||
|
||||
override fun registeredFlows(): List<String> = services.rpcFlows.map { it.name }.sorted()
|
||||
override fun registeredFlows(): List<String> = services.rpcFlows.asSequence().map(Class<*>::getName).sorted().toList()
|
||||
|
||||
override fun clearNetworkMapCache() {
|
||||
services.networkMapCache.clearNetworkMapCache()
|
||||
@ -271,18 +292,46 @@ internal class CordaRPCOpsImpl(
|
||||
return vaultTrackBy(criteria, PageSpecification(), sorting, contractStateType)
|
||||
}
|
||||
|
||||
override fun setFlowsDrainingModeEnabled(enabled: Boolean) {
|
||||
services.nodeProperties.flowsDrainingMode.setEnabled(enabled)
|
||||
override fun setFlowsDrainingModeEnabled(enabled: Boolean) = setPersistentDrainingModeProperty(enabled, propagateChange = true)
|
||||
|
||||
override fun isFlowsDrainingModeEnabled() = services.nodeProperties.flowsDrainingMode.isEnabled()
|
||||
|
||||
override fun shutdown() = terminate(false)
|
||||
|
||||
override fun terminate(drainPendingFlows: Boolean) {
|
||||
|
||||
if (drainPendingFlows) {
|
||||
logger.info("Waiting for pending flows to complete before shutting down.")
|
||||
setFlowsDrainingModeEnabled(true)
|
||||
drainingShutdownHook.set(pendingFlowsCount().updates.doOnNext {(completed, total) ->
|
||||
logger.info("Pending flows progress before shutdown: $completed / $total.")
|
||||
}.doOnCompleted { setPersistentDrainingModeProperty(false, false) }.doOnCompleted(::cancelDrainingShutdownHook).doOnCompleted { logger.info("No more pending flows to drain. Shutting down.") }.doOnCompleted(shutdownNode::invoke).subscribe({
|
||||
// Nothing to do on each update here, only completion matters.
|
||||
}, { error ->
|
||||
logger.error("Error while waiting for pending flows to drain in preparation for shutdown. Cause was: ${error.message}", error)
|
||||
}))
|
||||
} else {
|
||||
shutdownNode.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
override fun isFlowsDrainingModeEnabled(): Boolean {
|
||||
return services.nodeProperties.flowsDrainingMode.isEnabled()
|
||||
override fun isWaitingForShutdown() = drainingShutdownHook.get() != null
|
||||
|
||||
override fun close() {
|
||||
|
||||
cancelDrainingShutdownHook()
|
||||
}
|
||||
|
||||
override fun shutdown() {
|
||||
shutdownNode.invoke()
|
||||
private fun cancelDrainingShutdownHook() {
|
||||
|
||||
drainingShutdownHook.getAndSet(null)?.let {
|
||||
it.unsubscribe()
|
||||
logger.info("Cancelled draining shutdown hook.")
|
||||
}
|
||||
}
|
||||
|
||||
private fun setPersistentDrainingModeProperty(enabled: Boolean, propagateChange: Boolean) = services.nodeProperties.flowsDrainingMode.setEnabled(enabled, propagateChange)
|
||||
|
||||
private fun stateMachineInfoFromFlowLogic(flowLogic: FlowLogic<*>): StateMachineInfo {
|
||||
return StateMachineInfo(flowLogic.runId, flowLogic.javaClass.name, flowLogic.stateMachine.context.toFlowInitiator(), flowLogic.track(), flowLogic.stateMachine.context)
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ interface NodePropertiesStore {
|
||||
|
||||
interface FlowsDrainingModeOperations {
|
||||
|
||||
fun setEnabled(enabled: Boolean)
|
||||
fun setEnabled(enabled: Boolean, propagateChange: Boolean = true)
|
||||
|
||||
fun isEnabled(): Boolean
|
||||
|
||||
|
@ -57,12 +57,13 @@ class FlowsDrainingModeOperationsImpl(readPhysicalNodeId: () -> String, private
|
||||
|
||||
override val values = PublishSubject.create<Pair<Boolean, Boolean>>()!!
|
||||
|
||||
override fun setEnabled(enabled: Boolean) {
|
||||
var oldValue: Boolean? = null
|
||||
persistence.transaction {
|
||||
oldValue = map.put(nodeSpecificFlowsExecutionModeKey, enabled.toString())?.toBoolean() ?: false
|
||||
override fun setEnabled(enabled: Boolean, propagateChange: Boolean) {
|
||||
val oldValue = persistence.transaction {
|
||||
map.put(nodeSpecificFlowsExecutionModeKey, enabled.toString())?.toBoolean() ?: false
|
||||
}
|
||||
if (propagateChange) {
|
||||
values.onNext(oldValue to enabled)
|
||||
}
|
||||
values.onNext(oldValue!! to enabled)
|
||||
}
|
||||
|
||||
override fun isEnabled(): Boolean {
|
||||
|
Reference in New Issue
Block a user