Skip to content

Commit d17670c

Browse files
nimmajMike Hearn
authored andcommitted
Provide an API to register callback on app shutdown (#2402)
Provide an API to register callback on app shutdown.
1 parent 3c0e006 commit d17670c

File tree

7 files changed

+78
-12
lines changed

7 files changed

+78
-12
lines changed

core/src/main/kotlin/net/corda/core/node/ServiceHub.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,4 +354,19 @@ interface ServiceHub : ServicesForResolution {
354354
* @return A new [Connection]
355355
*/
356356
fun jdbcSession(): Connection
357+
358+
/**
359+
* Allows the registration of a callback that may inform services when the app is shutting down.
360+
*
361+
* The intent is to allow the cleaning up of resources - e.g. releasing ports.
362+
*
363+
* You should not rely on this to clean up executing flows - that's what quasar is for.
364+
*
365+
* Please note that the shutdown handler is not guaranteed to be called. In production the node process may crash,
366+
* be killed by the operating system and other forms of fatal termination may occur that result in this code never
367+
* running. So you should use this functionality only for unit/integration testing or for code that can optimise
368+
* this shutdown e.g. by cleaning up things that would otherwise trigger a slow recovery process next time the
369+
* node starts.
370+
*/
371+
fun registerUnloadHandler(runOnStop: () -> Unit)
357372
}

docs/source/changelog.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,9 @@ UNRELEASED
162162
However, assuming a clean reset of the artemis data and that the nodes are consistent versions,
163163
data persisted via the AMQP serializer will be forward compatible.
164164

165+
* The ability for CordaServices to register callbacks so they can be notified of shutdown and clean up resource such as
166+
open ports.
167+
165168
.. _changelog_v1:
166169

167170
Release 1.0

node/src/integration-test/kotlin/net/corda/node/NodePerformanceTests.kt

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,6 @@ import java.util.*
3131
import java.util.concurrent.TimeUnit
3232
import kotlin.streams.toList
3333

34-
35-
private fun checkQuasarAgent() {
36-
if (!(ManagementFactory.getRuntimeMXBean().inputArguments.any { it.contains("quasar") })) {
37-
throw IllegalStateException("No quasar agent")
38-
}
39-
}
40-
4134
@Ignore("Run these locally")
4235
class NodePerformanceTests {
4336
@StartableByRPC
@@ -52,11 +45,6 @@ class NodePerformanceTests {
5245
val averageMs: Double
5346
)
5447

55-
@Before
56-
fun before() {
57-
checkQuasarAgent()
58-
}
59-
6048
@Test
6149
fun `empty flow per second`() {
6250
driver(startNodesInProcess = true) {
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package net.corda.node
2+
3+
import net.corda.core.node.ServiceHub
4+
import net.corda.core.node.services.CordaService
5+
import net.corda.core.serialization.SingletonSerializeAsToken
6+
import net.corda.core.utilities.contextLogger
7+
import net.corda.core.utilities.getOrThrow
8+
import net.corda.testing.DUMMY_BANK_A_NAME
9+
import net.corda.testing.driver.driver
10+
import org.junit.Assert
11+
import org.junit.Test
12+
import java.util.concurrent.CountDownLatch
13+
import java.util.concurrent.TimeUnit
14+
15+
class NodeUnloadHandlerTests {
16+
17+
companion object {
18+
val latch = CountDownLatch(1)
19+
}
20+
21+
@Test
22+
fun `should be able to register run on stop lambda`() {
23+
driver(startNodesInProcess = true, extraCordappPackagesToScan = listOf("net.corda.node"), isDebug = true) {
24+
startNode(providedName = DUMMY_BANK_A_NAME).getOrThrow()
25+
// just want to fall off the end of this for the mo...
26+
}
27+
Assert.assertTrue("Timed out waiting for AbstractNode to invoke the test service shutdown callback",latch.await(30, TimeUnit.SECONDS))
28+
}
29+
30+
@CordaService
31+
class RunOnStopTestService(serviceHub: ServiceHub) : SingletonSerializeAsToken() {
32+
33+
companion object {
34+
private val log = contextLogger()
35+
}
36+
37+
init {
38+
serviceHub.registerUnloadHandler(this::shutdown)
39+
}
40+
41+
fun shutdown() {
42+
log.info("shutting down")
43+
latch.countDown()
44+
}
45+
46+
}
47+
48+
}

node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -804,6 +804,11 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
804804
}
805805

806806
override fun jdbcSession(): Connection = database.createSession()
807+
808+
// allows services to register handlers to be informed when the node stop method is called
809+
override fun registerUnloadHandler(handler: () -> Unit) {
810+
runOnStop += handler
811+
}
807812
}
808813
}
809814

testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ open class MockServices private constructor(
5757
private val initialIdentity: TestIdentity,
5858
private val moreKeys: Array<out KeyPair>
5959
) : ServiceHub, StateLoader by validatedTransactions {
60+
6061
companion object {
6162
@JvmStatic
6263
val MOCK_VERSION_INFO = VersionInfo(1, "Mock release", "Mock revision", "Mock Vendor")
@@ -157,6 +158,8 @@ open class MockServices private constructor(
157158
}
158159

159160
override fun jdbcSession(): Connection = throw UnsupportedOperationException()
161+
162+
override fun registerUnloadHandler(runOnStop: () -> Unit) = throw UnsupportedOperationException()
160163
}
161164

162165
class MockKeyManagementService(val identityService: IdentityServiceInternal,

testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import okhttp3.Request
5454
import rx.Observable
5555
import rx.observables.ConnectableObservable
5656
import rx.schedulers.Schedulers
57+
import java.lang.management.ManagementFactory
5758
import java.net.ConnectException
5859
import java.net.URL
5960
import java.net.URLClassLoader
@@ -737,6 +738,9 @@ class DriverDSLImpl(
737738
): CordaFuture<Pair<StartedNode<Node>, Thread>> {
738739
return executorService.fork {
739740
log.info("Starting in-process Node ${config.corda.myLegalName.organisation}")
741+
if (!(ManagementFactory.getRuntimeMXBean().inputArguments.any { it.contains("quasar") })) {
742+
throw IllegalStateException("No quasar agent: -javaagent:lib/quasar.jar and working directory project root might fix")
743+
}
740744
// Write node.conf
741745
writeConfig(config.corda.baseDirectory, "node.conf", config.typesafe)
742746
// TODO pass the version in?

0 commit comments

Comments
 (0)