[CORDA-1941]: Server-side draining node shutdown. (#3909)

This commit is contained in:
Michele Sollecito 2018-09-18 13:04:26 +02:00 committed by GitHub
parent 0b4d1c0f35
commit 5113f4c8c1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 275 additions and 112 deletions

View File

@ -3,9 +3,13 @@ package net.corda.client.rpc
import net.corda.core.CordaRuntimeException import net.corda.core.CordaRuntimeException
/** /**
* Thrown to indicate a fatal error in the RPC system itself, as opposed to an error generated by the invoked * Thrown to indicate a fatal error in the RPC system itself, as opposed to an error generated by the invoked method.
* method.
*/ */
open class RPCException(message: String?, cause: Throwable?) : CordaRuntimeException(message, cause) { open class RPCException(message: String?, cause: Throwable?) : CordaRuntimeException(message, cause) {
constructor(msg: String) : this(msg, null) constructor(msg: String) : this(msg, null)
} }
/**
* Signals that the underlying [RPCConnection] dropped.
*/
open class ConnectionFailureException(cause: Throwable? = null) : RPCException("Connection failure detected.", cause)

View File

@ -2,11 +2,8 @@ package net.corda.client.rpc.internal
import net.corda.client.rpc.CordaRPCClient import net.corda.client.rpc.CordaRPCClient
import net.corda.client.rpc.CordaRPCClientConfiguration import net.corda.client.rpc.CordaRPCClientConfiguration
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.messaging.pendingFlowsCount
import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.messaging.ClientRpcSslOptions import net.corda.core.messaging.ClientRpcSslOptions
import rx.Observable
/** Utility which exposes the internal Corda RPC constructor to other internal Corda components */ /** Utility which exposes the internal Corda RPC constructor to other internal Corda components */
fun createCordaRPCClientWithSslAndClassLoader( fun createCordaRPCClientWithSslAndClassLoader(
@ -14,14 +11,4 @@ fun createCordaRPCClientWithSslAndClassLoader(
configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT, configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT,
sslConfiguration: ClientRpcSslOptions? = null, sslConfiguration: ClientRpcSslOptions? = null,
classLoader: ClassLoader? = null classLoader: ClassLoader? = null
) = CordaRPCClient.createWithSslAndClassLoader(hostAndPort, configuration, sslConfiguration, classLoader) ) = CordaRPCClient.createWithSslAndClassLoader(hostAndPort, configuration, sslConfiguration, classLoader)
fun CordaRPCOps.drainAndShutdown(): Observable<Unit> {
setFlowsDrainingModeEnabled(true)
return pendingFlowsCount().updates
.doOnError { error ->
throw error
}
.doOnCompleted { shutdown() }.map { }
}

View File

@ -7,6 +7,7 @@ import com.github.benmanes.caffeine.cache.RemovalCause
import com.github.benmanes.caffeine.cache.RemovalListener import com.github.benmanes.caffeine.cache.RemovalListener
import com.google.common.util.concurrent.SettableFuture import com.google.common.util.concurrent.SettableFuture
import com.google.common.util.concurrent.ThreadFactoryBuilder import com.google.common.util.concurrent.ThreadFactoryBuilder
import net.corda.client.rpc.ConnectionFailureException
import net.corda.client.rpc.CordaRPCClientConfiguration import net.corda.client.rpc.CordaRPCClientConfiguration
import net.corda.client.rpc.RPCException import net.corda.client.rpc.RPCException
import net.corda.client.rpc.RPCSinceVersion import net.corda.client.rpc.RPCSinceVersion
@ -552,7 +553,7 @@ class RPCClientProxyHandler(
m.keys.forEach { k -> m.keys.forEach { k ->
observationExecutorPool.run(k) { observationExecutorPool.run(k) {
try { try {
m[k]?.onError(RPCException("Connection failure detected.")) m[k]?.onError(ConnectionFailureException())
} catch (th: Throwable) { } catch (th: Throwable) {
log.error("Unexpected exception when RPC connection failure handling", th) log.error("Unexpected exception when RPC connection failure handling", th)
} }
@ -561,7 +562,7 @@ class RPCClientProxyHandler(
observableContext.observableMap.invalidateAll() observableContext.observableMap.invalidateAll()
rpcReplyMap.forEach { _, replyFuture -> rpcReplyMap.forEach { _, replyFuture ->
replyFuture.setException(RPCException("Connection failure detected.")) replyFuture.setException(ConnectionFailureException())
} }
rpcReplyMap.clear() rpcReplyMap.clear()

View File

@ -22,7 +22,6 @@ import net.corda.core.serialization.CordaSerializable
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.Try import net.corda.core.utilities.Try
import rx.Observable import rx.Observable
import rx.subjects.PublishSubject
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.security.PublicKey import java.security.PublicKey
@ -405,38 +404,20 @@ interface CordaRPCOps : RPCOps {
* This does not wait for flows to be completed. * This does not wait for flows to be completed.
*/ */
fun shutdown() fun shutdown()
}
/** /**
* Returns a [DataFeed] that keeps track on the count of pending flows. * Shuts the node down. Returns immediately.
*/ * @param drainPendingFlows whether the node will wait for pending flows to be completed before exiting. While draining, new flows from RPC will be rejected.
fun CordaRPCOps.pendingFlowsCount(): DataFeed<Int, Pair<Int, Int>> { */
fun terminate(drainPendingFlows: Boolean = false)
val stateMachineState = stateMachinesFeed() /**
var pendingFlowsCount = stateMachineState.snapshot.size * Returns whether the node is waiting for pending flows to complete before shutting down.
var completedFlowsCount = 0 * Disabling draining mode cancels this state.
val updates = PublishSubject.create<Pair<Int, Int>>() *
stateMachineState * @return whether the node will shutdown when the pending flows count reaches zero.
.updates */
.doOnNext { update -> fun isWaitingForShutdown(): Boolean
when (update) {
is StateMachineUpdate.Added -> {
pendingFlowsCount++
updates.onNext(completedFlowsCount to pendingFlowsCount)
}
is StateMachineUpdate.Removed -> {
completedFlowsCount++
updates.onNext(completedFlowsCount to pendingFlowsCount)
if (completedFlowsCount == pendingFlowsCount) {
updates.onCompleted()
}
}
}
}.subscribe()
if (pendingFlowsCount == 0) {
updates.onCompleted()
}
return DataFeed(pendingFlowsCount, updates)
} }
inline fun <reified T : ContractState> CordaRPCOps.vaultQueryBy(criteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(), inline fun <reified T : ContractState> CordaRPCOps.vaultQueryBy(criteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(),

View File

@ -0,0 +1,52 @@
package net.corda.nodeapi.internal
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.messaging.DataFeed
import net.corda.core.messaging.StateMachineUpdate
import rx.Observable
import rx.schedulers.Schedulers
import rx.subjects.PublishSubject
import java.util.concurrent.TimeUnit
/**
* Returns a [DataFeed] of the number of pending flows. The [Observable] for the updates will complete the moment all pending flows will have terminated.
*/
fun CordaRPCOps.pendingFlowsCount(): DataFeed<Int, Pair<Int, Int>> {
val updates = PublishSubject.create<Pair<Int, Int>>()
val initialPendingFlowsCount = stateMachinesFeed().let {
var completedFlowsCount = 0
var pendingFlowsCount = it.snapshot.size
it.updates.observeOn(Schedulers.io()).subscribe({ update ->
when (update) {
is StateMachineUpdate.Added -> {
pendingFlowsCount++
updates.onNext(completedFlowsCount to pendingFlowsCount)
}
is StateMachineUpdate.Removed -> {
completedFlowsCount++
updates.onNext(completedFlowsCount to pendingFlowsCount)
if (completedFlowsCount == pendingFlowsCount) {
updates.onCompleted()
}
}
}
}, updates::onError)
if (pendingFlowsCount == 0) {
updates.onCompleted()
}
pendingFlowsCount
}
return DataFeed(initialPendingFlowsCount, updates)
}
/**
* Returns an [Observable] that will complete when the node will have cancelled the draining shutdown hook.
*
* @param interval the value of the polling interval, default is 5.
* @param unit the time unit of the polling interval, default is [TimeUnit.SECONDS].
*/
fun CordaRPCOps.hasCancelledDrainingShutdown(interval: Long = 5, unit: TimeUnit = TimeUnit.SECONDS): Observable<Unit> {
return Observable.interval(interval, unit).map { isWaitingForShutdown() }.takeFirst { waiting -> waiting == false }.map { Unit }
}

View File

@ -1,15 +1,20 @@
package net.corda.node.modes.draining package net.corda.node.modes.draining
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import net.corda.client.rpc.internal.drainAndShutdown import net.corda.core.flows.FlowLogic
import net.corda.core.flows.* 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.identity.Party
import net.corda.core.internal.concurrent.map import net.corda.core.internal.concurrent.map
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.messaging.startFlow import net.corda.core.messaging.startFlow
import net.corda.core.utilities.contextLogger import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.unwrap import net.corda.core.utilities.unwrap
import net.corda.node.services.Permissions import net.corda.node.services.Permissions
import net.corda.nodeapi.internal.hasCancelledDrainingShutdown
import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.BOB_NAME import net.corda.testing.core.BOB_NAME
import net.corda.testing.core.singleIdentity 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.driver.driver
import net.corda.testing.internal.chooseIdentity import net.corda.testing.internal.chooseIdentity
import net.corda.testing.node.User import net.corda.testing.node.User
import net.corda.testing.node.internal.waitForShutdown
import org.assertj.core.api.AssertionsForInterfaceTypes.assertThat import org.assertj.core.api.AssertionsForInterfaceTypes.assertThat
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import rx.Observable
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
import java.util.concurrent.Executors import java.util.concurrent.Executors
import java.util.concurrent.ScheduledExecutorService import java.util.concurrent.ScheduledExecutorService
@ -80,24 +87,74 @@ class P2PFlowsDrainingModeTest {
} }
@Test @Test
fun `clean shutdown by draining`() { fun `terminate node waiting for pending flows`() {
driver(DriverParameters(startNodesInProcess = true, portAllocation = portAllocation, notarySpecs = emptyList())) {
driver(DriverParameters(portAllocation = portAllocation, notarySpecs = emptyList())) {
val nodeA = startNode(providedName = ALICE_NAME, rpcUsers = users).getOrThrow() val nodeA = startNode(providedName = ALICE_NAME, rpcUsers = users).getOrThrow()
val nodeB = startNode(providedName = BOB_NAME, rpcUsers = users).getOrThrow() val nodeB = startNode(providedName = BOB_NAME, rpcUsers = users).getOrThrow()
var successful = false var successful = false
val latch = CountDownLatch(1) val latch = CountDownLatch(1)
nodeB.rpc.setFlowsDrainingModeEnabled(true) nodeB.rpc.setFlowsDrainingModeEnabled(true)
IntRange(1, 10).forEach { nodeA.rpc.startFlow(::InitiateSessionFlow, nodeB.nodeInfo.chooseIdentity()) } IntRange(1, 10).forEach { nodeA.rpc.startFlow(::InitiateSessionFlow, nodeB.nodeInfo.chooseIdentity()) }
nodeA.rpc.drainAndShutdown() nodeA.waitForShutdown().doOnError(Throwable::printStackTrace).doOnError { successful = false }.doOnCompleted { successful = true }.doAfterTerminate(latch::countDown).subscribe()
.doOnError { error ->
error.printStackTrace() nodeA.rpc.terminate(true)
successful = false
}
.doOnCompleted { successful = true }
.doAfterTerminate { latch.countDown() }
.subscribe()
nodeB.rpc.setFlowsDrainingModeEnabled(false) 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() latch.await()
assertThat(successful).isTrue() assertThat(successful).isTrue()

View File

@ -238,7 +238,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
/** The implementation of the [CordaRPCOps] interface used by this node. */ /** The implementation of the [CordaRPCOps] interface used by this node. */
open fun makeRPCOps(): CordaRPCOps { 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>() val proxies = mutableListOf<(CordaRPCOps) -> CordaRPCOps>()
// Mind that order is relevant here. // Mind that order is relevant here.
proxies += ::AuthenticatedRpcOpsProxy proxies += ::AuthenticatedRpcOpsProxy

View File

@ -28,17 +28,21 @@ import net.corda.core.node.services.vault.*
import net.corda.core.serialization.serialize import net.corda.core.serialization.serialize
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.getOrThrow 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.FlowStarter
import net.corda.node.services.api.ServiceHubInternal import net.corda.node.services.api.ServiceHubInternal
import net.corda.node.services.messaging.context import net.corda.node.services.messaging.context
import net.corda.node.services.statemachine.StateMachineManager import net.corda.node.services.statemachine.StateMachineManager
import net.corda.nodeapi.exceptions.NonRpcFlowException import net.corda.nodeapi.exceptions.NonRpcFlowException
import net.corda.nodeapi.exceptions.RejectedCommandException import net.corda.nodeapi.exceptions.RejectedCommandException
import net.corda.nodeapi.internal.pendingFlowsCount
import rx.Observable import rx.Observable
import rx.Subscription
import java.io.InputStream import java.io.InputStream
import java.net.ConnectException import java.net.ConnectException
import java.security.PublicKey import java.security.PublicKey
import java.time.Instant 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 * 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 smm: StateMachineManager,
private val flowStarter: FlowStarter, private val flowStarter: FlowStarter,
private val shutdownNode: () -> Unit 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 * Returns the RPC protocol version, which is the same the node's platform Version. Exists since version 1 so guaranteed
* to be present. * to be present.
@ -222,7 +243,7 @@ internal class CordaRPCOpsImpl(
return services.networkMapCache.getNodeByLegalIdentity(party) 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() { override fun clearNetworkMapCache() {
services.networkMapCache.clearNetworkMapCache() services.networkMapCache.clearNetworkMapCache()
@ -271,18 +292,46 @@ internal class CordaRPCOpsImpl(
return vaultTrackBy(criteria, PageSpecification(), sorting, contractStateType) return vaultTrackBy(criteria, PageSpecification(), sorting, contractStateType)
} }
override fun setFlowsDrainingModeEnabled(enabled: Boolean) { override fun setFlowsDrainingModeEnabled(enabled: Boolean) = setPersistentDrainingModeProperty(enabled, propagateChange = true)
services.nodeProperties.flowsDrainingMode.setEnabled(enabled)
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 { override fun isWaitingForShutdown() = drainingShutdownHook.get() != null
return services.nodeProperties.flowsDrainingMode.isEnabled()
override fun close() {
cancelDrainingShutdownHook()
} }
override fun shutdown() { private fun cancelDrainingShutdownHook() {
shutdownNode.invoke()
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 { private fun stateMachineInfoFromFlowLogic(flowLogic: FlowLogic<*>): StateMachineInfo {
return StateMachineInfo(flowLogic.runId, flowLogic.javaClass.name, flowLogic.stateMachine.context.toFlowInitiator(), flowLogic.track(), flowLogic.stateMachine.context) return StateMachineInfo(flowLogic.runId, flowLogic.javaClass.name, flowLogic.stateMachine.context.toFlowInitiator(), flowLogic.track(), flowLogic.stateMachine.context)
} }

View File

@ -8,7 +8,7 @@ interface NodePropertiesStore {
interface FlowsDrainingModeOperations { interface FlowsDrainingModeOperations {
fun setEnabled(enabled: Boolean) fun setEnabled(enabled: Boolean, propagateChange: Boolean = true)
fun isEnabled(): Boolean fun isEnabled(): Boolean

View File

@ -57,12 +57,13 @@ class FlowsDrainingModeOperationsImpl(readPhysicalNodeId: () -> String, private
override val values = PublishSubject.create<Pair<Boolean, Boolean>>()!! override val values = PublishSubject.create<Pair<Boolean, Boolean>>()!!
override fun setEnabled(enabled: Boolean) { override fun setEnabled(enabled: Boolean, propagateChange: Boolean) {
var oldValue: Boolean? = null val oldValue = persistence.transaction {
persistence.transaction { map.put(nodeSpecificFlowsExecutionModeKey, enabled.toString())?.toBoolean() ?: false
oldValue = map.put(nodeSpecificFlowsExecutionModeKey, enabled.toString())?.toBoolean() ?: false }
if (propagateChange) {
values.onNext(oldValue to enabled)
} }
values.onNext(oldValue!! to enabled)
} }
override fun isEnabled(): Boolean { override fun isEnabled(): Boolean {

View File

@ -54,8 +54,10 @@ import net.corda.testing.node.User
import net.corda.testing.node.internal.DriverDSLImpl.Companion.cordappsInCurrentAndAdditionalPackages import net.corda.testing.node.internal.DriverDSLImpl.Companion.cordappsInCurrentAndAdditionalPackages
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import rx.Observable
import rx.Subscription import rx.Subscription
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import rx.subjects.AsyncSubject
import java.lang.management.ManagementFactory import java.lang.management.ManagementFactory
import java.net.ConnectException import java.net.ConnectException
import java.net.URL import java.net.URL

View File

@ -1,5 +1,6 @@
package net.corda.testing.node.internal package net.corda.testing.node.internal
import net.corda.client.rpc.ConnectionFailureException
import net.corda.client.rpc.CordaRPCClient import net.corda.client.rpc.CordaRPCClient
import net.corda.core.CordaException import net.corda.core.CordaException
import net.corda.core.concurrent.CordaFuture import net.corda.core.concurrent.CordaFuture
@ -8,17 +9,21 @@ import net.corda.core.flows.FlowLogic
import net.corda.core.internal.FlowStateMachine import net.corda.core.internal.FlowStateMachine
import net.corda.core.internal.concurrent.openFuture import net.corda.core.internal.concurrent.openFuture
import net.corda.core.internal.times import net.corda.core.internal.times
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.millis import net.corda.core.utilities.millis
import net.corda.core.utilities.seconds import net.corda.core.utilities.seconds
import net.corda.node.services.api.StartedNodeServices import net.corda.node.services.api.StartedNodeServices
import net.corda.node.services.messaging.Message import net.corda.node.services.messaging.Message
import net.corda.testing.driver.NodeHandle
import net.corda.testing.internal.chooseIdentity import net.corda.testing.internal.chooseIdentity
import net.corda.testing.node.InMemoryMessagingNetwork import net.corda.testing.node.InMemoryMessagingNetwork
import net.corda.testing.node.User import net.corda.testing.node.User
import net.corda.testing.node.testContext import net.corda.testing.node.testContext
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import rx.Observable
import rx.subjects.AsyncSubject
import java.net.Socket import java.net.Socket
import java.net.SocketException import java.net.SocketException
import java.time.Duration import java.time.Duration
@ -108,4 +113,22 @@ fun StartedNodeServices.newContext(): InvocationContext = testContext(myInfo.cho
fun InMemoryMessagingNetwork.MessageTransfer.getMessage(): Message = message fun InMemoryMessagingNetwork.MessageTransfer.getMessage(): Message = message
fun CordaRPCClient.start(user: User) = start(user.username, user.password) fun CordaRPCClient.start(user: User) = start(user.username, user.password)
fun NodeHandle.waitForShutdown(): Observable<Unit> {
return rpc.waitForShutdown().doAfterTerminate(::stop)
}
fun CordaRPCOps.waitForShutdown(): Observable<Unit> {
val completable = AsyncSubject.create<Unit>()
stateMachinesFeed().updates.subscribe({ _ -> }, { error ->
if (error is ConnectionFailureException) {
completable.onCompleted()
} else {
completable.onError(error)
}
})
return completable
}

View File

@ -44,7 +44,7 @@ public class RunShellCommand extends InteractiveShellCommand {
emitHelp(context, parser); emitHelp(context, parser);
return null; return null;
} }
return InteractiveShell.runRPCFromString(command, out, context, ops(), objectMapper(), isSsh()); return InteractiveShell.runRPCFromString(command, out, context, ops(), objectMapper());
} }
private void emitHelp(InvocationContext<Map> context, StringToMethodCallParser<CordaRPCOps> parser) { private void emitHelp(InvocationContext<Map> context, StringToMethodCallParser<CordaRPCOps> parser) {

View File

@ -18,6 +18,7 @@ import net.corda.core.internal.*
import net.corda.core.internal.concurrent.doneFuture import net.corda.core.internal.concurrent.doneFuture
import net.corda.core.internal.concurrent.openFuture import net.corda.core.internal.concurrent.openFuture
import net.corda.core.messaging.* import net.corda.core.messaging.*
import net.corda.nodeapi.internal.pendingFlowsCount
import net.corda.tools.shell.utlities.ANSIProgressRenderer import net.corda.tools.shell.utlities.ANSIProgressRenderer
import net.corda.tools.shell.utlities.StdoutANSIProgressRenderer import net.corda.tools.shell.utlities.StdoutANSIProgressRenderer
import org.crsh.command.InvocationContext import org.crsh.command.InvocationContext
@ -408,7 +409,7 @@ object InteractiveShell {
} }
@JvmStatic @JvmStatic
fun runRPCFromString(input: List<String>, out: RenderPrintWriter, context: InvocationContext<out Any>, cordaRPCOps: CordaRPCOps, om: ObjectMapper, isSsh: Boolean = false): Any? { fun runRPCFromString(input: List<String>, out: RenderPrintWriter, context: InvocationContext<out Any>, cordaRPCOps: CordaRPCOps, om: ObjectMapper): Any? {
val cmd = input.joinToString(" ").trim { it <= ' ' } val cmd = input.joinToString(" ").trim { it <= ' ' }
if (cmd.startsWith("startflow", ignoreCase = true)) { if (cmd.startsWith("startflow", ignoreCase = true)) {
// The flow command provides better support and startFlow requires special handling anyway due to // The flow command provides better support and startFlow requires special handling anyway due to
@ -417,7 +418,7 @@ object InteractiveShell {
out.println("Please use the 'flow' command to interact with flows rather than the 'run' command.", Color.yellow) out.println("Please use the 'flow' command to interact with flows rather than the 'run' command.", Color.yellow)
return null return null
} else if (cmd.substringAfter(" ").trim().equals("gracefulShutdown", ignoreCase = true)) { } else if (cmd.substringAfter(" ").trim().equals("gracefulShutdown", ignoreCase = true)) {
return InteractiveShell.gracefulShutdown(out, cordaRPCOps, isSsh) return InteractiveShell.gracefulShutdown(out, cordaRPCOps)
} }
var result: Any? = null var result: Any? = null
@ -456,9 +457,8 @@ object InteractiveShell {
return result return result
} }
@JvmStatic @JvmStatic
fun gracefulShutdown(userSessionOut: RenderPrintWriter, cordaRPCOps: CordaRPCOps, isSsh: Boolean = false) { fun gracefulShutdown(userSessionOut: RenderPrintWriter, cordaRPCOps: CordaRPCOps) {
fun display(statements: RenderPrintWriter.() -> Unit) { fun display(statements: RenderPrintWriter.() -> Unit) {
statements.invoke(userSessionOut) statements.invoke(userSessionOut)
@ -467,40 +467,48 @@ object InteractiveShell {
var isShuttingDown = false var isShuttingDown = false
try { try {
display { println("Orchestrating a clean shutdown, press CTRL+C to cancel...") }
isShuttingDown = true
display { display {
println("Orchestrating a clean shutdown...")
println("...enabling draining mode") println("...enabling draining mode")
}
cordaRPCOps.setFlowsDrainingModeEnabled(true)
display {
println("...waiting for in-flight flows to be completed") println("...waiting for in-flight flows to be completed")
} }
cordaRPCOps.pendingFlowsCount().updates cordaRPCOps.terminate(true)
.doOnError { error ->
log.error(error.message) val latch = CountDownLatch(1)
throw error cordaRPCOps.pendingFlowsCount().updates.doOnError { error ->
} log.error(error.message)
.doOnNext { (first, second) -> throw error
display { }.doAfterTerminate(latch::countDown).subscribe(
println("...remaining: ${first}/${second}") // For each update.
{ (first, second) -> display { println("...remaining: $first / $second") } },
// On error.
{ error ->
if (!isShuttingDown) {
display { println("RPC failed: ${error.rootCause}", Color.red) }
} }
} },
.doOnCompleted { // When completed.
if (isSsh) { {
// print in the original Shell process
System.out.println("Shutting down the node via remote SSH session (it may take a while)")
}
display {
println("Shutting down the node (it may take a while)")
}
cordaRPCOps.shutdown()
isShuttingDown = true
connection.forceClose() connection.forceClose()
display { // This will only show up in the standalone Shell, because the embedded one is killed as part of a node's shutdown.
println("...done, quitting standalone shell now.") display { println("...done, quitting the shell now.") }
}
onExit.invoke() onExit.invoke()
}.toBlocking().single() })
while (!Thread.currentThread().isInterrupted) {
try {
latch.await()
break
} catch (e: InterruptedException) {
try {
cordaRPCOps.setFlowsDrainingModeEnabled(false)
display { println("...cancelled clean shutdown.") }
} finally {
Thread.currentThread().interrupt()
break
}
}
}
} catch (e: StringToMethodCallParser.UnparseableCallException) { } catch (e: StringToMethodCallParser.UnparseableCallException) {
display { display {
println(e.message, Color.red) println(e.message, Color.red)
@ -508,9 +516,7 @@ object InteractiveShell {
} }
} catch (e: Exception) { } catch (e: Exception) {
if (!isShuttingDown) { if (!isShuttingDown) {
display { display { println("RPC failed: ${e.rootCause}", Color.red) }
println("RPC failed: ${e.rootCause}", Color.red)
}
} }
} finally { } finally {
InputStreamSerializer.invokeContext = null InputStreamSerializer.invokeContext = null