CORDA-2942: Node lifecycle events (#5846)

* CORDA-2942: Port minimal set of changes to make lifecycle events work

... and make codebase compile.

* CORDA-2942: Undo some changes which are not strictly speaking necessary

* CORDA-2942: Make `NodeServicesContext` leaner and delete `extensions-api` module

* CORDA-2942: Reduce even more number of files affected

* CORDA-2942: Integration test fix

* CORDA-2942: Make events `AfterStart` and `BeforeStop` generic w.r.t. `NodeServicesContext`

* CORDA-2942: `NodeLifecycleObserverService` and a set of integration tests.

Public API violations are expected as well as integration tests failing.

* CORDA-2942: Re-work to introduce `ServiceLifecycleObserver`

* CORDA-2942: Explicitly mention a type of exception that may be thrown for some events.

* CORDA-2942: Register `ServiceLifecycleObserver` through `AppServiceHub`

* CORDA-2942: Fix integration test + KDocs update

* CORDA-2942: Detekt and `api-current` update

* CORDA-2942: Improvement to `CordaServiceLifecycleFatalTests`

... or else it has side effects on other tests.

* CORDA-2942: Add an integration test for new API use in Java

Driver test is written in Kotlin, but services definition is written in Java.

Also KDocs improvements.

* CORDA-2942: Documentation and release notes update

* CORDA-2942: First set of changes following review by @mnesbit

* CORDA-2942: Second set of changes following review by @mnesbit

* CORDA-2942: Added multi-threaded test

* CORDA-2942: Fixes

* CORDA-2942: Undo changes to `api-current.txt`

* CORDA-2942: Bare mimimum change to `api-current.txt` for CI gate to pass.

* CORDA-2942: Address review feedback from @rick-r3

* CORDA-2942: Detekt update

* CORDA-2942: Delete `ServiceLifecycleObserverPriority` and replace it with `Int` after discussion with @mnesbit

* CORDA-2942: Introduce more `NodeLifecycleEvent` and switch services to listen for those events

* CORDA-2942: Few more changes after input from @rick-r3

* First stub on integration test
Unfinished - hang on issue and pay

* CORDA-2942: Switch to use out-of-process nodes for the inetgration test

Currently Alice and Notary stuck waiting to hear from each other.

* CORDA-2942: Extra log lines during event distribution

* CORDA-2942: Asynchronously distribute lifecycle events

* CORDA-2942: Await for complete P2P client start-up

Next step: Add vault query to integration test

* CORDA-2942: Asynchronously distribute lifecycle events

Next step: Improve integration test

* CORDA-2942: Fix test broken by recent changes and improve logging

* CORDA-2942: Improvement of the test to be able to monitor actions performed by @CordaService in the remote process

* CORDA-2942: Add node re-start step to the integration test

* CORDA-2942: Remove `CORDAPP_STOPPED` event for now

* CORDA-2942: s/CORDAPP_STARTED/STATE_MACHINE_STARTED/

* CORDA-2942: Inverse the meaning of `priority` as requested by @rick-r3

* CORDA-2942: Register `AppServiceHubImpl` for lifecycle events and put a warning when SMM is not ready.
This commit is contained in:
Viktor Kolomeyko 2020-01-21 13:38:02 +00:00 committed by Rick Parker
parent a4d00b79d4
commit 0978500a9a
36 changed files with 1055 additions and 164 deletions

View File

@ -3754,6 +3754,9 @@ public static final class net.corda.core.node.services.PartyInfo$SingleNode exte
@NotNull
public String toString()
##
public interface net.corda.core.node.services.ServiceLifecycleObserver
public abstract void onServiceLifecycleEvent(net.corda.core.node.services.ServiceLifecycleEvent)
##
@CordaSerializable
public final class net.corda.core.node.services.StatesNotAvailableException extends net.corda.core.flows.FlowException
public <init>(String, Throwable)

View File

@ -64,4 +64,20 @@ internal fun ConfigValue.serialize(options: ConfigRenderOptions = ConfigRenderOp
internal typealias Valid<TARGET> = Validated<TARGET, Configuration.Validation.Error>
internal fun <TYPE> valid(target: TYPE) = Validated.valid<TYPE, Configuration.Validation.Error>(target)
internal fun <TYPE> valid(target: TYPE) = Validated.valid<TYPE, Configuration.Validation.Error>(target)
/**
* Value extracted from a configuration file is a function of the actual value specified and configuration options.
* E.g. password value may be stored in the encrypted form rather than in a clear text.
*/
data class ConfigurationWithOptions(private val config: Config, private val options: Configuration.Validation.Options) {
operator fun <TYPE> get(property: Configuration.Property.Definition<TYPE>): TYPE = property.valueIn(config)
operator fun <TYPE> get(property: Configuration.Value.Extractor<TYPE>): TYPE = property.valueIn(config)
}
/**
* Helper interface to mark objects that will have [ConfigurationWithOptions] in them.
*/
interface ConfigurationWithOptionsContainer {
val configurationWithOptions : ConfigurationWithOptions
}

View File

@ -4,6 +4,8 @@ import net.corda.core.DeleteForDJVM
import net.corda.core.flows.FlowLogic
import net.corda.core.messaging.FlowHandle
import net.corda.core.messaging.FlowProgressHandle
import net.corda.core.node.services.ServiceLifecycleEvent
import net.corda.core.node.services.ServiceLifecycleObserver
import net.corda.core.node.services.vault.CordaTransactionSupport
import rx.Observable
@ -17,6 +19,12 @@ import rx.Observable
@DeleteForDJVM
interface AppServiceHub : ServiceHub {
companion object {
const val SERVICE_PRIORITY_HIGH = 200
const val SERVICE_PRIORITY_NORMAL = 100
const val SERVICE_PRIORITY_LOW = 20
}
/**
* Start the given flow with the given arguments. [flow] must be annotated
* with [net.corda.core.flows.StartableByService].
@ -38,4 +46,24 @@ interface AppServiceHub : ServiceHub {
* independent transaction from those used in the framework.
*/
val database: CordaTransactionSupport
}
/**
* Allows to register [ServiceLifecycleObserver] such that it will start receiving [net.corda.core.node.services.ServiceLifecycleEvent]s
*
* @param priority controls to which queue [observer] will be added. Higher values correspond to higher priorities.
*
* @param observer an instance of [ServiceLifecycleObserver] to be registered.
*/
fun register(priority: Int = SERVICE_PRIORITY_NORMAL, observer: ServiceLifecycleObserver)
/**
* Convenience method to be able to add an arbitrary function as a [register] callback.
*/
fun <T> register(priority: Int = SERVICE_PRIORITY_NORMAL,
func: (ServiceLifecycleEvent) -> T) = register(priority,
object : ServiceLifecycleObserver {
override fun onServiceLifecycleEvent(event: ServiceLifecycleEvent) {
func(event)
}
})
}

View File

@ -0,0 +1,36 @@
package net.corda.core.node.services
import java.lang.Exception
/**
* Specifies that given [CordaService] is interested to know about important milestones of Corda Node lifecycle and potentially react to them.
* Subscription can be performed using [net.corda.core.node.AppServiceHub.register] method from a constructor of [CordaService].
*/
@FunctionalInterface
interface ServiceLifecycleObserver {
/**
* A handler for [ServiceLifecycleEvent]s.
* Default implementation does nothing.
*/
@Throws(CordaServiceCriticalFailureException::class)
fun onServiceLifecycleEvent(event: ServiceLifecycleEvent)
}
enum class ServiceLifecycleEvent {
/**
* This event is dispatched when State Machine is fully started such that [net.corda.core.node.AppServiceHub] available
* for [CordaService] to be use.
*
* If a handler for this event throws [CordaServiceCriticalFailureException] - this is the way to flag that it will not make
* sense for Corda node to continue its operation. The lifecycle events dispatcher will endeavor to terminate node's JVM as soon
* as practically possible.
*/
STATE_MACHINE_STARTED,
}
/**
* Please see [ServiceLifecycleEvent.STATE_MACHINE_STARTED] for the purpose of this exception.
*/
class CordaServiceCriticalFailureException(message : String, cause: Throwable?) : Exception(message, cause) {
constructor(message : String) : this(message, null)
}

View File

@ -117,8 +117,8 @@
<ID>ComplexMethod:CheckpointAgent.kt$CheckpointHook$private fun instrumentClass(clazz: CtClass): CtClass?</ID>
<ID>ComplexMethod:CheckpointAgent.kt$CheckpointHook$private fun prettyStatsTree(indent: Int, statsInfo: StatsInfo, identityInfo: IdentityInfo, builder: StringBuilder)</ID>
<ID>ComplexMethod:CheckpointAgent.kt$fun readTrees(events: List&lt;StatsEvent&gt;, index: Int, idMap: IdentityHashMap&lt;Any, IdentityInfo&gt;): Pair&lt;Int, List&lt;Pair&lt;StatsInfo, IdentityInfo&gt;&gt;&gt;</ID>
<ID>ComplexMethod:CheckpointDumper.kt$CheckpointDumper$fun dump()</ID>
<ID>ComplexMethod:CheckpointDumper.kt$CheckpointDumper$private fun FlowIORequest&lt;*&gt;.toSuspendedOn(suspendedTimestamp: Instant, now: Instant): SuspendedOn</ID>
<ID>ComplexMethod:CheckpointDumperImpl.kt$CheckpointDumperImpl$fun dumpCheckpoints()</ID>
<ID>ComplexMethod:CheckpointDumperImpl.kt$CheckpointDumperImpl$private fun FlowIORequest&lt;*&gt;.toSuspendedOn(suspendedTimestamp: Instant, now: Instant): SuspendedOn</ID>
<ID>ComplexMethod:ClassCarpenter.kt$ClassCarpenterImpl$ private fun validateSchema(schema: Schema)</ID>
<ID>ComplexMethod:CompatibleTransactionTests.kt$CompatibleTransactionTests$@Test fun `Command visibility tests`()</ID>
<ID>ComplexMethod:ConfigUtilities.kt$// For Iterables figure out the type parameter and apply the same logic as above on the individual elements. private fun Iterable&lt;*&gt;.toConfigIterable(field: Field): Iterable&lt;Any?&gt;</ID>
@ -190,15 +190,11 @@
<ID>ComplexMethod:SendTransactionFlow.kt$DataVendingFlow$@Suspendable override fun call(): Void?</ID>
<ID>ComplexMethod:ShellCmdLineOptions.kt$ShellCmdLineOptions$private fun toConfigFile(): Config</ID>
<ID>ComplexMethod:ShellCmdLineOptions.kt$ShellConfigurationFile.ShellConfigFile$fun toShellConfiguration(): ShellConfiguration</ID>
<ID>ComplexMethod:SignedTransaction.kt$SignedTransaction$// TODO: Verify contract constraints here as well as in LedgerTransaction to ensure that anything being deserialised // from the attachment is trusted. This will require some partial serialisation work to not load the ContractState // objects from the TransactionState. @DeleteForDJVM private fun verifyRegularTransaction(services: ServiceHub, checkSufficientSignatures: Boolean)</ID>
<ID>ComplexMethod:StartedFlowTransition.kt$StartedFlowTransition$override fun transition(): TransitionResult</ID>
<ID>ComplexMethod:StatusTransitions.kt$StatusTransitions$ fun verify(tx: LedgerTransaction)</ID>
<ID>ComplexMethod:StringToMethodCallParser.kt$StringToMethodCallParser$ @Throws(UnparseableCallException::class) fun parse(target: T?, command: String): ParsedMethodCall</ID>
<ID>ComplexMethod:TlsDiffAlgorithmsTest.kt$TlsDiffAlgorithmsTest$@Test fun testClientServerTlsExchange()</ID>
<ID>ComplexMethod:TlsDiffProtocolsTest.kt$TlsDiffProtocolsTest$@Test fun testClientServerTlsExchange()</ID>
<ID>ComplexMethod:TransactionBuilder.kt$TransactionBuilder$ fun withItems(vararg items: Any)</ID>
<ID>ComplexMethod:TransactionBuilder.kt$TransactionBuilder$ private fun addMissingDependency(services: ServicesForResolution, wireTx: WireTransaction): Boolean</ID>
<ID>ComplexMethod:TransactionBuilder.kt$TransactionBuilder$ private fun handleContract( contractClassName: ContractClassName, inputStates: List&lt;TransactionState&lt;ContractState&gt;&gt;?, outputStates: List&lt;TransactionState&lt;ContractState&gt;&gt;?, explicitContractAttachment: AttachmentId?, services: ServicesForResolution ): Pair&lt;AttachmentId, List&lt;TransactionState&lt;ContractState&gt;&gt;?&gt;</ID>
<ID>ComplexMethod:TransactionUtils.kt$ fun createComponentGroups(inputs: List&lt;StateRef&gt;, outputs: List&lt;TransactionState&lt;ContractState&gt;&gt;, commands: List&lt;Command&lt;*&gt;&gt;, attachments: List&lt;SecureHash&gt;, notary: Party?, timeWindow: TimeWindow?, references: List&lt;StateRef&gt;, networkParametersHash: SecureHash?): List&lt;ComponentGroup&gt;</ID>
<ID>ComplexMethod:TypeModellingFingerPrinter.kt$FingerPrintingState$// For a type we haven't seen before, determine the correct path depending on the type of type it is. private fun fingerprintNewType(type: LocalTypeInformation)</ID>
<ID>ComplexMethod:UniversalContract.kt$UniversalContract$override fun verify(tx: LedgerTransaction)</ID>
@ -1308,8 +1304,7 @@
<ID>MaxLineLength:AbstractNode.kt$AbstractNode$throw IllegalStateException("CryptoService and signingCertificateStore are not aligned, the entry for key-alias: $alias is only found in $keyExistsIn")</ID>
<ID>MaxLineLength:AbstractNode.kt$AbstractNode$val cordappProvider = CordappProviderImpl(cordappLoader, CordappConfigFileProvider(configuration.cordappDirectories), attachments).tokenize()</ID>
<ID>MaxLineLength:AbstractNode.kt$AbstractNode$val servicesForResolution = ServicesForResolutionImpl(identityService, attachments, cordappProvider, networkParametersStorage, transactionStorage).also { attachments.servicesForResolution = it }</ID>
<ID>MaxLineLength:AbstractNode.kt$AbstractNode.AppServiceHubImpl$override val database: CordaTransactionSupport</ID>
<ID>MaxLineLength:AbstractNode.kt$AbstractNode.AppServiceHubImpl$require(logicType.isAnnotationPresent(StartableByService::class.java)) { "${logicType.name} was not designed for starting by a CordaService" }</ID>
<ID>MaxLineLength:AbstractNode.kt$AbstractNode.&lt;no name provided&gt;$// Note: tokenizableServices passed by reference meaning that any subsequent modification to the content in the `AbstractNode` will // be reflected in the context as well. However, since context only has access to immutable collection it can only read (but not modify) // the content. override val tokenizableServices: List&lt;SerializeAsToken&gt; = this@AbstractNode.tokenizableServices!!</ID>
<ID>MaxLineLength:AbstractNode.kt$ex is HikariPool.PoolInitializationException -&gt; throw CouldNotCreateDataSourceException("Could not connect to the database. Please check your JDBC connection URL, or the connectivity to the database.", ex)</ID>
<ID>MaxLineLength:AbstractNode.kt$ex.cause is ClassNotFoundException -&gt; throw CouldNotCreateDataSourceException("Could not find the database driver class. Please add it to the 'drivers' folder. See: https://docs.corda.net/corda-configuration-file.html")</ID>
<ID>MaxLineLength:AbstractNode.kt$fun CordaPersistence.startHikariPool(hikariProperties: Properties, databaseConfig: DatabaseConfig, schemas: Set&lt;MappedSchema&gt;, metricRegistry: MetricRegistry? = null, cordappLoader: CordappLoader? = null, currentDir: Path? = null, ourName: CordaX500Name)</ID>
@ -1342,6 +1337,8 @@
<ID>MaxLineLength:AnalyticsEngine.kt$OGSIMMAnalyticsEngine$val t = BimmAnalysisUtils.computeMargin(combinedRatesProvider, normalizer, calculatorTotal, it.value.currencyParameterSensitivities, it.value.multiCurrencyAmount)</ID>
<ID>MaxLineLength:AnonymousParty.kt$AnonymousParty : DestinationAbstractParty</ID>
<ID>MaxLineLength:AnotherDummyContract.kt$AnotherDummyContract$return TransactionBuilder(notary).withItems(StateAndContract(state, ANOTHER_DUMMY_PROGRAM_ID), Command(Commands.Create(), owner.party.owningKey))</ID>
<ID>MaxLineLength:AppServiceHubImpl.kt$AppServiceHubImpl$require(logicType.isAnnotationPresent(StartableByService::class.java)) { "${logicType.name} was not designed for starting by a CordaService" }</ID>
<ID>MaxLineLength:AppServiceHubImpl.kt$AppServiceHubImpl.Companion.NodeLifecycleServiceObserverAdaptor$private</ID>
<ID>MaxLineLength:AppendOnlyPersistentMap.kt$AppendOnlyPersistentMapBase$ operator fun set(key: K, value: V)</ID>
<ID>MaxLineLength:AppendOnlyPersistentMap.kt$AppendOnlyPersistentMapBase$log.warn("Double insert in ${this.javaClass.name} for entity class $persistentEntityClass key $key, not inserting the second time")</ID>
<ID>MaxLineLength:AppendOnlyPersistentMap.kt$AppendOnlyPersistentMapBase$oldValueInCache</ID>
@ -1598,9 +1595,8 @@
<ID>MaxLineLength:CheckpointAgent.kt$log.debug { "Skipping repeated StatsEvent.Enter: ${exit.value} (hashcode:${exit.value!!.hashCode()}) (count:${idMap[exit.value]?.refCount})" }</ID>
<ID>MaxLineLength:CheckpointAgent.kt$log.debug { "Skipping repeated StatsEvent.Exit: ${event.value} (hashcode:${event.value!!.hashCode()}) (count:${idMap[event.value]?.refCount})" }</ID>
<ID>MaxLineLength:CheckpointAgent.kt$log.debug { "Skipping repeated StatsEvent.ObjectField: ${event.value} (hashcode:${event.value.hashCode()}) (count:${idMap[event.value]?.refCount})" }</ID>
<ID>MaxLineLength:CheckpointDumper.kt$CheckpointDumper</ID>
<ID>MaxLineLength:CheckpointDumper.kt$CheckpointDumper.CheckpointDumperBeanModifier$it.type.isTypeOrSubTypeOf(ProgressTracker::class.java) || it.name == "_stateMachine" || it.name == "deprecatedPartySessionMap"</ID>
<ID>MaxLineLength:CheckpointDumperTest.kt$CheckpointDumperTest$val checkpoint = Checkpoint.create(InvocationContext.shell(), FlowStart.Explicit, logic.javaClass, frozenLogic, myself.identity.party, SubFlowVersion.CoreFlow(version), false) .getOrThrow()</ID>
<ID>MaxLineLength:CheckpointDumperImpl.kt$CheckpointDumperImpl.CheckpointDumperBeanModifier$it.type.isTypeOrSubTypeOf(ProgressTracker::class.java) || it.name == "_stateMachine" || it.name == "deprecatedPartySessionMap"</ID>
<ID>MaxLineLength:CheckpointDumperImplTest.kt$CheckpointDumperImplTest$val checkpoint = Checkpoint.create(InvocationContext.shell(), FlowStart.Explicit, logic.javaClass, frozenLogic, myself.identity.party, SubFlowVersion.CoreFlow(version), false) .getOrThrow()</ID>
<ID>MaxLineLength:CheckpointSerializationScheme.kt$CheckpointSerializationContextImpl$override val encodingWhitelist: EncodingWhitelist = NullEncodingWhitelist</ID>
<ID>MaxLineLength:CheckpointVerifier.kt$CheckpointIncompatibleException$FlowVersionIncompatibleException : CheckpointIncompatibleException</ID>
<ID>MaxLineLength:CheckpointVerifier.kt$CheckpointIncompatibleException$SubFlowCoreVersionIncompatibleException : CheckpointIncompatibleException</ID>
@ -2223,7 +2219,6 @@
<ID>MaxLineLength:IRSDemoDockerTest.kt$IRSDemoDockerTest.Companion$DockerComposeRule.builder() .files(DockerComposeFiles.from( System.getProperty("CORDAPP_DOCKER_COMPOSE"), System.getProperty("WEB_DOCKER_COMPOSE"))) .waitingForService("web-a", HealthChecks.toRespondOverHttp(8080, { port -&gt; port.inFormat("http://\$HOST:\$EXTERNAL_PORT") }))</ID>
<ID>MaxLineLength:IRSDemoTest.kt$IRSDemoTest$val (controllerApi, nodeAApi, nodeBApi) = listOf(controller, nodeA, nodeB).zip(listOf(controllerAddr, nodeAAddr, nodeBAddr)).map { val mapper = JacksonSupport.createDefaultMapper(it.first.rpc) registerFinanceJSONMappers(mapper) registerIRSModule(mapper) HttpApi.fromHostAndPort(it.second, "api/irs", mapper = mapper) }</ID>
<ID>MaxLineLength:IRSDemoTest.kt$IRSDemoTest.InterestRateSwapStateDeserializer$InterestRateSwap.State(fixedLeg = fixedLeg, floatingLeg = floatingLeg, calculation = calculation, common = common, linearId = linearId, oracle = oracle)</ID>
<ID>MaxLineLength:IRSState.kt$IRSState$return TransactionBuilder(notary).withItems(StateAndContract(state, IRS_PROGRAM_ID), Command(OGTrade.Commands.Agree(), participants.map { it.owningKey }))</ID>
<ID>MaxLineLength:IRSTests.kt$"(floatingLeg.notional.pennies * (calculation.fixingSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value))"</ID>
<ID>MaxLineLength:IRSTests.kt$( // TODO: this seems to fail quite dramatically //expression = "fixedLeg.notional * fixedLeg.fixedRate", // TODO: How I want it to look //expression = "( fixedLeg.notional * (fixedLeg.fixedRate)) - (floatingLeg.notional * (rateSchedule.get(context.getDate('currentDate'))))", // How it's ended up looking, which I think is now broken but it's a WIP. expression = Expression("( fixedLeg.notional.pennies * (fixedLeg.fixedRate.ratioUnit.value)) -" + "(floatingLeg.notional.pennies * (calculation.fixingSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value))"), floatingLegPaymentSchedule = mutableMapOf(), fixedLegPaymentSchedule = mutableMapOf() )</ID>
<ID>MaxLineLength:IRSTests.kt$IRSTests$( "fixedLeg.notional.quantity", "fixedLeg.fixedRate.ratioUnit", "fixedLeg.fixedRate.ratioUnit.value", "floatingLeg.notional.quantity", "fixedLeg.fixedRate", "currentBusinessDate", "calculation.floatingLegPaymentSchedule.get(currentBusinessDate)", "fixedLeg.notional.token.currencyCode", "fixedLeg.notional.quantity * 10", "fixedLeg.notional.quantity * fixedLeg.fixedRate.ratioUnit.value", "(fixedLeg.notional.token.currencyCode.equals('GBP')) ? 365 : 360 ", "(fixedLeg.notional.quantity * (fixedLeg.fixedRate.ratioUnit.value))" // "calculation.floatingLegPaymentSchedule.get(context.getDate('currentDate')).rate" // "calculation.floatingLegPaymentSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value", //"( fixedLeg.notional.pennies * (fixedLeg.fixedRate.ratioUnit.value)) - (floatingLeg.notional.pennies * (calculation.fixingSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value))", // "( fixedLeg.notional * fixedLeg.fixedRate )" )</ID>
@ -2269,7 +2264,6 @@
<ID>MaxLineLength:InMemoryMessagingNetwork.kt$InMemoryMessagingNetwork$peersMapping[messagingService.myAddress.name] = messagingService.myAddress</ID>
<ID>MaxLineLength:InMemoryMessagingNetwork.kt$InMemoryMessagingNetwork$private val servicePeerAllocationStrategy: ServicePeerAllocationStrategy = InMemoryMessagingNetwork.ServicePeerAllocationStrategy.Random()</ID>
<ID>MaxLineLength:InMemoryMessagingNetwork.kt$InMemoryMessagingNetwork.Companion$servicePeerAllocationStrategy: ServicePeerAllocationStrategy = InMemoryMessagingNetwork.ServicePeerAllocationStrategy.Random()</ID>
<ID>MaxLineLength:InMemoryTransactionVerifierService.kt$InMemoryTransactionVerifierService : SingletonSerializeAsTokenTransactionVerifierServiceTransactionVerifierServiceInternalAutoCloseable</ID>
<ID>MaxLineLength:InfrequentlyMutatedCacheTest.kt$InfrequentlyMutatedCacheTest$@Test fun `fourth get outside first transaction from empty cache with invalidate in other thread in the middle returns result of second loader`()</ID>
<ID>MaxLineLength:InitialRegistrationCli.kt$InitialRegistration : RunAfterNodeInitialisationNodeStartupLogging</ID>
<ID>MaxLineLength:InitialRegistrationCli.kt$InitialRegistration$println("Node was started before with `--initial-registration`, but the registration was not completed.\nResuming registration.")</ID>
@ -2663,6 +2657,7 @@
<ID>MaxLineLength:NodeKeystoreCheckTest.kt$NodeKeystoreCheckTest$setPrivateKey(X509Utilities.CORDA_CLIENT_CA, nodeCA.keyPair.private, listOf(badNodeCACert, badRoot), signingCertStore.entryPassword)</ID>
<ID>MaxLineLength:NodeKeystoreCheckTest.kt$NodeKeystoreCheckTest$val badNodeCACert = X509Utilities.createCertificate(CertificateType.NODE_CA, badRoot, badRootKeyPair, ALICE_NAME.x500Principal, nodeCA.keyPair.public)</ID>
<ID>MaxLineLength:NodeKeystoreCheckTest.kt$NodeKeystoreCheckTest$val p2pSslConfig = CertificateStoreStubs.P2P.withCertificatesDirectory(certificatesDirectory, keyStorePassword = keystorePassword, trustStorePassword = keystorePassword)</ID>
<ID>MaxLineLength:NodeLifecycleEventsDistributor.kt$NodeLifecycleEventsDistributor</ID>
<ID>MaxLineLength:NodeMonitorModel.kt$NodeMonitorModel${ rpc = CordaRPCClient(nodeHostAndPort).start(username, password, GracefulReconnect()) proxyObservable.value = rpc.proxy // Vault snapshot (force single page load with MAX_PAGE_SIZE) + updates val ( statesSnapshot, vaultUpdates ) = rpc.proxy.vaultTrackBy&lt;ContractState&gt;( QueryCriteria.VaultQueryCriteria(Vault.StateStatus.ALL), PageSpecification(DEFAULT_PAGE_NUM, MAX_PAGE_SIZE) ) val unconsumedStates = statesSnapshot.states.filterIndexed { index, _ -&gt; statesSnapshot.statesMetadata[index].status == Vault.StateStatus.UNCONSUMED }.toSet() val consumedStates = statesSnapshot.states.toSet() - unconsumedStates val initialVaultUpdate = Vault.Update(consumedStates, unconsumedStates, references = emptySet()) vaultUpdates.startWith(initialVaultUpdate).subscribe(vaultUpdatesSubject::onNext) // Transactions val (transactions, newTransactions) = @Suppress("DEPRECATION") rpc.proxy.internalVerifiedTransactionsFeed() newTransactions.startWith(transactions).subscribe(transactionsSubject::onNext) // SM -&gt; TX mapping val (smTxMappings, futureSmTxMappings) = rpc.proxy.stateMachineRecordedTransactionMappingFeed() futureSmTxMappings.startWith(smTxMappings).subscribe(stateMachineTransactionMappingSubject::onNext) // Parties on network val (parties, futurePartyUpdate) = rpc.proxy.networkMapFeed() futurePartyUpdate.startWith(parties.map(MapChange::Added)).subscribe(networkMapSubject::onNext) val stateMachines = rpc.proxy.stateMachinesSnapshot() notaryIdentities = rpc.proxy.notaryIdentities() // Extract the flow tracking stream // TODO is there a nicer way of doing this? Stream of streams in general results in code like this... // TODO `progressTrackingSubject` doesn't seem to be used anymore - should it be removed? val currentProgressTrackerUpdates = stateMachines.mapNotNull { stateMachine -&gt; ProgressTrackingEvent.createStreamFromStateMachineInfo(stateMachine) } val futureProgressTrackerUpdates = stateMachineUpdatesSubject.map { stateMachineUpdate -&gt; if (stateMachineUpdate is StateMachineUpdate.Added) { ProgressTrackingEvent.createStreamFromStateMachineInfo(stateMachineUpdate.stateMachineInfo) ?: Observable.empty&lt;ProgressTrackingEvent&gt;() } else { Observable.empty&lt;ProgressTrackingEvent&gt;() } } // We need to retry, because when flow errors, we unsubscribe from progressTrackingSubject. So we end up with stream of state machine updates and no progress trackers. futureProgressTrackerUpdates.startWith(currentProgressTrackerUpdates).flatMap { it }.retry().subscribe(progressTrackingSubject) }</ID>
<ID>MaxLineLength:NodeNamedCache.kt$DefaultNamedCacheFactory$name.startsWith("RPCSecurityManagerShiroCache_") -&gt; with(security?.authService?.options?.cache!!) { caffeine.maximumSize(maxEntries).expireAfterWrite(expireAfterSecs, TimeUnit.SECONDS) }</ID>
<ID>MaxLineLength:NodeNamedCache.kt$DefaultNamedCacheFactory$open</ID>
@ -2934,7 +2929,6 @@
<ID>MaxLineLength:PortfolioApiUtils.kt$PortfolioApiUtils$InitialMarginView</ID>
<ID>MaxLineLength:PortfolioApiUtils.kt$PortfolioApiUtils$val processedSensitivities = valuation.totalSensivities.sensitivities.map { it.marketDataName to it.parameterMetadata.map { it.label }.zip(it.sensitivity.toList()).toMap() }.toMap()</ID>
<ID>MaxLineLength:PortfolioApiUtils.kt$PortfolioApiUtils$val yieldCurveCurrenciesValues = marketData.filter { !it.key.contains("/") }.map { it -&gt; Triple(it.key.split("-")[0], it.key.split("-", limit = 2)[1], it.value) }</ID>
<ID>MaxLineLength:PortfolioState.kt$PortfolioState$return TransactionBuilder(notary).withItems(StateAndContract(copy(), PORTFOLIO_SWAP_PROGRAM_ID), Command(PortfolioSwap.Commands.Agree(), participants.map { it.owningKey }))</ID>
<ID>MaxLineLength:PrintingInterceptor.kt$PrintingInterceptor$val transitionRecord = TransitionDiagnosticRecord(Instant.now(), fiber.id, previousState, nextState, event, transition, continuation)</ID>
<ID>MaxLineLength:ProgressTracker.kt$ProgressTracker$log.warnOnce("Found ProgressTracker Step(s) with the same label: ${labels.groupBy { it }.filter { it.value.size &gt; 1 }.map { it.key }}")</ID>
<ID>MaxLineLength:ProgressTracker.kt$ProgressTracker.Step$private fun definitionLocation(): String</ID>
@ -3200,6 +3194,7 @@
<ID>MaxLineLength:ServiceHub.kt$ServiceHub$private</ID>
<ID>MaxLineLength:ServiceHub.kt$ServiceHub$signInitialTransaction(builder, publicKey, SignatureMetadata(myInfo.platformVersion, Crypto.findSignatureScheme(publicKey).schemeNumberID))</ID>
<ID>MaxLineLength:ServiceHubInternal.kt$ServiceHubInternal.Companion$vaultService.notifyAll(statesToRecord, recordedTransactions.map { it.coreTransaction }, previouslySeenTxs.map { it.coreTransaction })</ID>
<ID>MaxLineLength:ServiceLifecycleObserver.kt$ServiceLifecycleObserver</ID>
<ID>MaxLineLength:ServicesForResolutionImpl.kt$ServicesForResolutionImpl$else -&gt; throw UnsupportedOperationException("Attempting to resolve attachment for index ${stateRef.index} of a ${ctx.javaClass} transaction. This is not supported.")</ID>
<ID>MaxLineLength:ServicesForResolutionImpl.kt$ServicesForResolutionImpl$if (attachment is ContractAttachment &amp;&amp; (forContractClassName ?: transactionState.contract) in attachment.allContracts) { return attachment }</ID>
<ID>MaxLineLength:ServicesForResolutionImpl.kt$ServicesForResolutionImpl$return attachments.openAttachment(ctx.upgradedContractAttachmentId) ?: throw AttachmentResolutionException(stateRef.txhash)</ID>
@ -3212,8 +3207,6 @@
<ID>MaxLineLength:SignatureConstraintMigrationFromWhitelistConstraintTests.kt$SignatureConstraintMigrationFromWhitelistConstraintTests$@Test fun `auto migration from WhitelistConstraint to SignatureConstraint will only transition states that do not have a constraint specified`()</ID>
<ID>MaxLineLength:SignedNodeInfo.kt$NodeInfoAndSigned$constructor(nodeInfo: NodeInfo, signer: (PublicKey, SerializedBytes&lt;NodeInfo&gt;) -&gt; DigitalSignature) : this(nodeInfo, nodeInfo.sign(signer))</ID>
<ID>MaxLineLength:SignedTransaction.kt$SignedTransaction : TransactionWithSignatures</ID>
<ID>MaxLineLength:SignedTransaction.kt$SignedTransaction$ |If you wish to verify this transaction, please contact the originator of the transaction and install the provided missing JAR.</ID>
<ID>MaxLineLength:SignedTransaction.kt$SignedTransaction$"""Transaction $ltx is incorrectly formed. Most likely it was created during version 3 of Corda when the verification logic was more lenient. |Attempted to find local dependency for class: $missingClass, but could not find one. |If you wish to verify this transaction, please contact the originator of the transaction and install the provided missing JAR. |You can install it using the RPC command: `uploadAttachment` without restarting the node. |"""</ID>
<ID>MaxLineLength:SignedTransaction.kt$SignedTransaction$?:</ID>
<ID>MaxLineLength:SignedTransaction.kt$SignedTransaction$@Throws(SignatureException::class, AttachmentResolutionException::class, TransactionResolutionException::class, TransactionVerificationException::class)</ID>
<ID>MaxLineLength:SignedTransaction.kt$SignedTransaction$SignaturesMissingException : NamedByHashSignatureExceptionCordaThrowable</ID>
@ -3389,11 +3382,8 @@
<ID>MaxLineLength:TransactionBuilder.kt$TransactionBuilder$ private fun attachmentConstraintsTransition( constraints: Set&lt;AttachmentConstraint&gt;, attachmentToUse: ContractAttachment, services: ServicesForResolution ): AttachmentConstraint</ID>
<ID>MaxLineLength:TransactionBuilder.kt$TransactionBuilder$ private fun handleContract( contractClassName: ContractClassName, inputStates: List&lt;TransactionState&lt;ContractState&gt;&gt;?, outputStates: List&lt;TransactionState&lt;ContractState&gt;&gt;?, explicitContractAttachment: AttachmentId?, services: ServicesForResolution ): Pair&lt;AttachmentId, List&lt;TransactionState&lt;ContractState&gt;&gt;?&gt;</ID>
<ID>MaxLineLength:TransactionBuilder.kt$TransactionBuilder$ private fun selectAttachmentConstraint( contractClassName: ContractClassName, inputStates: List&lt;TransactionState&lt;ContractState&gt;&gt;?, attachmentToUse: ContractAttachment, services: ServicesForResolution): AttachmentConstraint</ID>
<ID>MaxLineLength:TransactionBuilder.kt$TransactionBuilder$ private fun selectContractAttachmentsAndOutputStateConstraints( services: ServicesForResolution, @Suppress("UNUSED_PARAMETER") serializationContext: SerializationContext? ): Pair&lt;Collection&lt;SecureHash&gt;, List&lt;TransactionState&lt;ContractState&gt;&gt;&gt;</ID>
<ID>MaxLineLength:TransactionBuilder.kt$TransactionBuilder$"An attachment has been explicitly set for contract $contractClassName in the transaction builder which conflicts with the HashConstraint of a state."</ID>
<ID>MaxLineLength:TransactionBuilder.kt$TransactionBuilder$"Transaction was built with $contractClassName states with multiple HashConstraints. This is illegal, because it makes it impossible to validate with a single version of the contract code."</ID>
<ID>MaxLineLength:TransactionBuilder.kt$TransactionBuilder$(allContractAttachments + attachments).toSortedSet().toList()</ID>
<ID>MaxLineLength:TransactionBuilder.kt$TransactionBuilder$@CordaInternal internal</ID>
<ID>MaxLineLength:TransactionBuilder.kt$TransactionBuilder$constraints.any { it is WhitelistedByZoneAttachmentConstraint } &amp;&amp; attachmentToUse.isSigned &amp;&amp; services.networkParameters.minimumPlatformVersion &gt;= 4 -&gt; transitionToSignatureConstraint(constraints, attachmentToUse)</ID>
<ID>MaxLineLength:TransactionBuilder.kt$TransactionBuilder$if ((attachment as ContractAttachment).isSigned &amp;&amp; (explicitContractAttachment == null || explicitContractAttachment == attachment.id)) { val signatureConstraint = makeSignatureAttachmentConstraint(attachment.signerKeys) require(signatureConstraint.isSatisfiedBy(attachment)) { "Selected output constraint: $signatureConstraint not satisfying ${attachment.id}" } val resolvedOutputStates = outputStates?.map { if (it.constraint in automaticConstraints) { it.copy(constraint = signatureConstraint) } else { it } } return attachment.id to resolvedOutputStates }</ID>
<ID>MaxLineLength:TransactionBuilder.kt$TransactionBuilder$internal</ID>
@ -3403,17 +3393,14 @@
<ID>MaxLineLength:TransactionBuilder.kt$TransactionBuilder$private fun useWhitelistedByZoneAttachmentConstraint(contractClassName: ContractClassName, networkParameters: NetworkParameters)</ID>
<ID>MaxLineLength:TransactionBuilder.kt$TransactionBuilder$require(automaticConstraintPropagation) { "Contract $contractClassName was marked with @NoConstraintPropagation, which means the constraint of the output states has to be set explicitly." }</ID>
<ID>MaxLineLength:TransactionBuilder.kt$TransactionBuilder$require(defaultOutputConstraint.isSatisfiedBy(constraintAttachment)) { "Selected output constraint: $defaultOutputConstraint not satisfying $selectedAttachmentId" }</ID>
<ID>MaxLineLength:TransactionBuilder.kt$TransactionBuilder$require(explicitAttachmentContracts.isEmpty() || explicitAttachmentContracts.groupBy { (ctr, _) -&gt; ctr }.all { (_, groups) -&gt; groups.size == 1 }) { "Multiple attachments set for the same contract." }</ID>
<ID>MaxLineLength:TransactionBuilder.kt$TransactionBuilder$require(outputConstraint.canBeTransitionedFrom(input.constraint, attachmentToUse)) { "Output state constraint $outputConstraint cannot be transitioned from ${input.constraint}" }</ID>
<ID>MaxLineLength:TransactionBuilder.kt$TransactionBuilder$require(signatureConstraint.isSatisfiedBy(attachment)) { "Selected output constraint: $signatureConstraint not satisfying ${attachment.id}" }</ID>
<ID>MaxLineLength:TransactionBuilder.kt$TransactionBuilder$throw IllegalArgumentException("Attempting to create an illegal transaction. Please install the latest signed version for the $attachmentToUse Cordapp.")</ID>
<ID>MaxLineLength:TransactionBuilder.kt$TransactionBuilder$throw IllegalArgumentException("Can't mix the AlwaysAcceptAttachmentConstraint with a secure constraint in the same transaction. This can be used to hide insecure transitions.")</ID>
<ID>MaxLineLength:TransactionBuilder.kt$TransactionBuilder$val (allContractAttachments: Collection&lt;SecureHash&gt;, resolvedOutputs: List&lt;TransactionState&lt;ContractState&gt;&gt;) = selectContractAttachmentsAndOutputStateConstraints(services, serializationContext)</ID>
<ID>MaxLineLength:TransactionBuilder.kt$TransactionBuilder$val attachments: Collection&lt;AttachmentId&gt; = contractAttachmentsAndResolvedOutputStates.map { it.first } + refStateContractAttachments</ID>
<ID>MaxLineLength:TransactionBuilder.kt$TransactionBuilder$val automaticConstraintPropagation = contractClassName.contractHasAutomaticConstraintPropagation(inputsAndOutputs.first().data::class.java.classLoader)</ID>
<ID>MaxLineLength:TransactionBuilder.kt$TransactionBuilder$val constraintAttachment = AttachmentWithContext(attachmentToUse, contractClassName, services.networkParameters.whitelistedContractImplementations)</ID>
<ID>MaxLineLength:TransactionBuilder.kt$TransactionBuilder$val contractAttachmentsAndResolvedOutputStates: List&lt;Pair&lt;AttachmentId, List&lt;TransactionState&lt;ContractState&gt;&gt;?&gt;&gt; = allContracts.toSet() .map { ctr -&gt; handleContract(ctr, inputContractGroups[ctr], outputContractGroups[ctr], explicitAttachmentContractsMap[ctr], services) }</ID>
<ID>MaxLineLength:TransactionBuilder.kt$TransactionBuilder$val referenceStateGroups: Map&lt;ContractClassName, List&lt;TransactionState&lt;ContractState&gt;&gt;&gt; = referencesWithTransactionState.groupBy { it.contract }</ID>
<ID>MaxLineLength:TransactionBuilder.kt$TransactionBuilder$val resolvedOutputStatesInTheOriginalOrder: List&lt;TransactionState&lt;ContractState&gt;&gt; = outputStates().map { os -&gt; resolvedStates.find { rs -&gt; rs.data == os.data &amp;&amp; rs.encumbrance == os.encumbrance }!! }</ID>
<ID>MaxLineLength:TransactionBuilder.kt$TransactionBuilder$when { // Sanity check. constraints.isEmpty() -&gt; throw IllegalArgumentException("Cannot transition from no constraints.") // Fail when combining the insecure AlwaysAcceptAttachmentConstraint with something else. constraints.size &gt; 1 &amp;&amp; constraints.any { it is AlwaysAcceptAttachmentConstraint } -&gt; throw IllegalArgumentException("Can't mix the AlwaysAcceptAttachmentConstraint with a secure constraint in the same transaction. This can be used to hide insecure transitions.") // Multiple states with Hash constraints with different hashes. This should not happen as we checked already. constraints.size &gt; 1 &amp;&amp; constraints.all { it is HashAttachmentConstraint } -&gt; throw IllegalArgumentException("Cannot mix HashConstraints with different hashes in the same transaction.") // The HashAttachmentConstraint is the strongest constraint, so it wins when mixed with anything. As long as the actual constraints pass. // Migration from HashAttachmentConstraint to SignatureAttachmentConstraint is handled in [TransactionBuilder.handleContract] // If we have reached this point, then no migration is possible and the existing HashAttachmentConstraint must be used constraints.any { it is HashAttachmentConstraint } -&gt; constraints.find { it is HashAttachmentConstraint }!! // TODO, we don't currently support mixing signature constraints with different signers. This will change once we introduce third party signers. constraints.count { it is SignatureAttachmentConstraint } &gt; 1 -&gt; throw IllegalArgumentException("Cannot mix SignatureAttachmentConstraints signed by different parties in the same transaction.") // This ensures a smooth migration from a Whitelist Constraint to a Signature Constraint constraints.any { it is WhitelistedByZoneAttachmentConstraint } &amp;&amp; attachmentToUse.isSigned &amp;&amp; services.networkParameters.minimumPlatformVersion &gt;= 4 -&gt; transitionToSignatureConstraint(constraints, attachmentToUse) // This condition is hit when the current node has not installed the latest signed version but has already received states that have been migrated constraints.any { it is SignatureAttachmentConstraint } &amp;&amp; !attachmentToUse.isSigned -&gt; throw IllegalArgumentException("Attempting to create an illegal transaction. Please install the latest signed version for the $attachmentToUse Cordapp.") // When all input states have the same constraint. constraints.size == 1 -&gt; constraints.single() else -&gt; throw IllegalArgumentException("Unexpected constraints $constraints.") }</ID>
<ID>MaxLineLength:TransactionBuilder.kt$TransactionBuilder${ // If the constraint on the output state is already set, and is not a valid transition or can't be transitioned, then fail early. inputStates?.forEach { input -&gt; require(outputConstraint.canBeTransitionedFrom(input.constraint, attachmentToUse)) { "Output state constraint $outputConstraint cannot be transitioned from ${input.constraint}" } } require(outputConstraint.isSatisfiedBy(constraintAttachment)) { "Output state constraint check fails. $outputConstraint" } it }</ID>
@ -3752,7 +3739,6 @@
<ID>NestedBlockDepth:StatusTransitions.kt$StatusTransitions$ fun verify(tx: LedgerTransaction)</ID>
<ID>NestedBlockDepth:ThrowableSerializer.kt$ThrowableSerializer$override fun fromProxy(proxy: ThrowableProxy): Throwable</ID>
<ID>NestedBlockDepth:TransactionVerifierServiceInternal.kt$Verifier$ private fun verifyConstraintsValidity(contractAttachmentsByContract: Map&lt;ContractClassName, ContractAttachment&gt;)</ID>
<ID>SpreadOperator:AMQPSerializationScheme.kt$AbstractAMQPSerializationScheme$(*it.whitelist.toTypedArray())</ID>
<ID>SpreadOperator:AbstractNode.kt$FlowStarterImpl$(logicType, *args)</ID>
<ID>SpreadOperator:AbstractParty.kt$AbstractParty$(*bytes)</ID>
<ID>SpreadOperator:AbstractRPCTest.kt$AbstractRPCTest.Companion$(*modes)</ID>
@ -3929,15 +3915,11 @@
<ID>ThrowsCount:SchemaMigration.kt$SchemaMigration$private fun doRunMigration( run: Boolean, check: Boolean, existingCheckpoints: Boolean? = null )</ID>
<ID>ThrowsCount:ServicesForResolutionImpl.kt$ServicesForResolutionImpl$// We may need to recursively chase transactions if there are notary changes. fun inner(stateRef: StateRef, forContractClassName: String?): Attachment</ID>
<ID>ThrowsCount:SignedNodeInfo.kt$SignedNodeInfo$// TODO Add root cert param (or TrustAnchor) to make sure all the identities belong to the same root fun verified(): NodeInfo</ID>
<ID>ThrowsCount:SignedTransaction.kt$SignedTransaction$// TODO: Verify contract constraints here as well as in LedgerTransaction to ensure that anything being deserialised // from the attachment is trusted. This will require some partial serialisation work to not load the ContractState // objects from the TransactionState. @DeleteForDJVM private fun verifyRegularTransaction(services: ServiceHub, checkSufficientSignatures: Boolean)</ID>
<ID>ThrowsCount:SignedTransaction.kt$SignedTransaction$@DeleteForDJVM private fun resolveAndCheckNetworkParameters(services: ServiceHub)</ID>
<ID>ThrowsCount:SingleThreadedStateMachineManager.kt$SingleThreadedStateMachineManager$private fun getInitiatedFlowFactory(message: InitialSessionMessage): InitiatedFlowFactory&lt;*&gt;</ID>
<ID>ThrowsCount:StringToMethodCallParser.kt$StringToMethodCallParser$ @Throws(UnparseableCallException::class) fun parse(target: T?, command: String): ParsedMethodCall</ID>
<ID>ThrowsCount:StringToMethodCallParser.kt$StringToMethodCallParser$ @Throws(UnparseableCallException::class) fun parseArguments(methodNameHint: String, parameters: List&lt;Pair&lt;String, Type&gt;&gt;, args: String): Array&lt;Any?&gt;</ID>
<ID>ThrowsCount:StructuresTests.kt$AttachmentTest$@Test fun `openAsJAR does not leak file handle if attachment has corrupted manifest`()</ID>
<ID>ThrowsCount:TransactionBuilder.kt$TransactionBuilder$ fun withItems(vararg items: Any)</ID>
<ID>ThrowsCount:TransactionBuilder.kt$TransactionBuilder$ private fun addMissingDependency(services: ServicesForResolution, wireTx: WireTransaction): Boolean</ID>
<ID>ThrowsCount:TransactionBuilder.kt$TransactionBuilder$ private fun attachmentConstraintsTransition( constraints: Set&lt;AttachmentConstraint&gt;, attachmentToUse: ContractAttachment, services: ServicesForResolution ): AttachmentConstraint</ID>
<ID>ThrowsCount:TransactionVerifierServiceInternal.kt$Verifier$ private fun getUniqueContractAttachmentsByContract(): Map&lt;ContractClassName, ContractAttachment&gt;</ID>
<ID>ThrowsCount:TransactionVerifierServiceInternal.kt$Verifier$// Using basic graph theory, a full cycle of encumbered (co-dependent) states should exist to achieve bi-directional // encumbrances. This property is important to ensure that no states involved in an encumbrance-relationship // can be spent on their own. Briefly, if any of the states is having more than one encumbrance references by // other states, a full cycle detection will fail. As a result, all of the encumbered states must be present // as "from" and "to" only once (or zero times if no encumbrance takes place). For instance, // a -&gt; b // c -&gt; b and a -&gt; b // b -&gt; a b -&gt; c // do not satisfy the bi-directionality (full cycle) property. // // In the first example "b" appears twice in encumbrance ("to") list and "c" exists in the encumbered ("from") list only. // Due the above, one could consume "a" and "b" in the same transaction and then, because "b" is already consumed, "c" cannot be spent. // // Similarly, the second example does not form a full cycle because "a" and "c" exist in one of the lists only. // As a result, one can consume "b" and "c" in the same transactions, which will make "a" impossible to be spent. // // On other hand the following are valid constructions: // a -&gt; b a -&gt; c // b -&gt; c and c -&gt; b // c -&gt; a b -&gt; a // and form a full cycle, meaning that the bi-directionality property is satisfied. private fun checkBidirectionalOutputEncumbrances(statesAndEncumbrance: List&lt;Pair&lt;Int, Int&gt;&gt;)</ID>
<ID>ThrowsCount:WireTransaction.kt$WireTransaction$private fun toLedgerTransactionInternal( resolveIdentity: (PublicKey) -&gt; Party?, resolveAttachment: (SecureHash) -&gt; Attachment?, resolveStateRefAsSerialized: (StateRef) -&gt; SerializedBytes&lt;TransactionState&lt;ContractState&gt;&gt;?, resolveParameters: (SecureHash?) -&gt; NetworkParameters?, isAttachmentTrusted: (Attachment) -&gt; Boolean ): LedgerTransaction</ID>
@ -3963,7 +3945,7 @@
<ID>TooGenericExceptionCaught:CertRole.kt$CertRole.Companion$ex: ArrayIndexOutOfBoundsException</ID>
<ID>TooGenericExceptionCaught:CheckpointAgent.kt$CheckpointAgent.Companion$e: Exception</ID>
<ID>TooGenericExceptionCaught:CheckpointAgent.kt$CheckpointHook$throwable: Throwable</ID>
<ID>TooGenericExceptionCaught:CheckpointDumper.kt$CheckpointDumper$e: Exception</ID>
<ID>TooGenericExceptionCaught:CheckpointDumperImpl.kt$CheckpointDumperImpl$e: Exception</ID>
<ID>TooGenericExceptionCaught:CheckpointVerifier.kt$CheckpointVerifier$e: Exception</ID>
<ID>TooGenericExceptionCaught:CollectSignaturesFlow.kt$SignTransactionFlow$e: Exception</ID>
<ID>TooGenericExceptionCaught:ConcurrencyUtils.kt$t: Throwable</ID>
@ -4422,9 +4404,9 @@
<ID>WildcardImport:CertificateRevocationListNodeTests.kt$import net.corda.nodeapi.internal.crypto.*</ID>
<ID>WildcardImport:CertificateRevocationListNodeTests.kt$import org.bouncycastle.asn1.x509.*</ID>
<ID>WildcardImport:CertificatesUtils.kt$import net.corda.nodeapi.internal.crypto.*</ID>
<ID>WildcardImport:CheckpointDumper.kt$import com.fasterxml.jackson.databind.*</ID>
<ID>WildcardImport:CheckpointDumper.kt$import net.corda.core.internal.*</ID>
<ID>WildcardImport:CheckpointDumper.kt$import net.corda.node.services.statemachine.*</ID>
<ID>WildcardImport:CheckpointDumperImpl.kt$import com.fasterxml.jackson.databind.*</ID>
<ID>WildcardImport:CheckpointDumperImpl.kt$import net.corda.core.internal.*</ID>
<ID>WildcardImport:CheckpointDumperImpl.kt$import net.corda.node.services.statemachine.*</ID>
<ID>WildcardImport:CheckpointSerializationAPI.kt$import net.corda.core.serialization.*</ID>
<ID>WildcardImport:ClassCarpenter.kt$import org.objectweb.asm.Opcodes.*</ID>
<ID>WildcardImport:ClassCarpenterTestUtils.kt$import net.corda.serialization.internal.amqp.*</ID>

View File

@ -32,7 +32,23 @@ Below is an empty implementation of a Service class:
class MyCordaService(private val serviceHub: AppServiceHub) : SingletonSerializeAsToken() {
init {
// code ran at service creation / node startup
// Custom code ran at service creation
// Optional: Express interest in receiving lifecycle events
services.register { processEvent(it) }
}
private fun processEvent(event: ServiceLifecycleEvent) {
// Lifecycle event handling code including full use of serviceHub
when (event) {
STATE_MACHINE_STARTED -> {
services.vaultService.queryBy(...)
services.startFlow(...)
}
else -> {
// Process other types of events
}
}
}
// public api of service
@ -43,11 +59,26 @@ Below is an empty implementation of a Service class:
@CordaService
public class MyCordaService extends SingletonSerializeAsToken {
private AppServiceHub serviceHub;
private final AppServiceHub serviceHub;
public MyCordaService(AppServiceHub serviceHub) {
this.serviceHub = serviceHub;
// code ran at service creation / node startup
// Custom code ran at service creation
// Optional: Express interest in receiving lifecycle events
serviceHub.register(SERVICE_PRIORITY_NORMAL, this::processEvent);
}
private void processEvent(ServiceLifecycleEvent event) {
switch (event) {
case STATE_MACHINE_STARTED:
serviceHub.getVaultService().queryBy(...)
serviceHub.startFlow(...)
break;
default:
// Process other types of events
break;
}
}
// public api of service
@ -59,9 +90,9 @@ from ``AppServiceHub`` is explained further in :ref:`Starting Flows from a Servi
The ``AppServiceHub`` also provides access to ``database`` which will enable the Service class to perform DB transactions from the threads
managed by the Service.
Code can be run during node startup when the class is being initialised. To do so, place the code into the ``init`` block or constructor.
This is useful when a service needs to establish a connection to an external database or setup observables via ``ServiceHub.trackBy`` during
its startup. These can then persist during the service's lifetime.
Also the ``AppServiceHub`` provides ability for ``CordaService`` to subscribe for lifecycle events of the node, such that it will get notified
about node finishing initialisation and when the node is shutting down such that ``CordaService`` will be able to perform clean-up of some
critical resources. For more details please have refer to KDocs for ``ServiceLifecycleObserver``.
Retrieving a Service
--------------------
@ -96,8 +127,4 @@ starting. To avoid this, the rules bellow should be followed:
.. note:: It is possible to avoid deadlock without following these rules depending on the number of flows running within the node. But, if the
number of flows violating these rules reaches the flow worker queue size, then the node will deadlock. It is best practice to
abide by these rules to remove this possibility.
abide by these rules to remove this possibility.

View File

@ -7,11 +7,12 @@ release, see :doc:`app-upgrade-notes`.
Unreleased
----------
* Added ability for ``CordaService`` to register for receiving node lifecycle events to perform initialisation and clean-up actions.
* Custom serializer classes implementing ``SerializationCustomSerializer`` should ideally be packaged in the same CorDapp as the
contract classes. Corda 4 could therefore fail to verify transactions created with Corda 3 if their custom serializer classes
had been packaged somewhere else. Add a "fallback mechanism" to Corda's transaction verification logic which will attempt to
include any missing custom serializers from other CorDapps within ``AttachmentStorage``.
* ``AppServiceHub`` been extended to provide access to ``database`` which will enable the Service class to perform DB transactions
from the threads managed by the custom Service.

View File

@ -0,0 +1,10 @@
package net.corda.nodeapi.internal.lifecycle
import net.corda.common.configuration.parsing.internal.ConfigurationWithOptionsContainer
/**
* Bare minimum information which will be available even before node fully started-up.
*/
interface NodeInitialContext : ConfigurationWithOptionsContainer {
val platformVersion: Int
}

View File

@ -0,0 +1,128 @@
package net.corda.nodeapi.internal.lifecycle
import com.google.common.util.concurrent.ThreadFactoryBuilder
import net.corda.core.concurrent.CordaFuture
import net.corda.core.internal.concurrent.map
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 java.util.Collections.singleton
import java.util.LinkedList
import java.util.concurrent.Executors
import java.util.concurrent.locks.Lock
import java.util.concurrent.locks.ReadWriteLock
import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.system.exitProcess
/**
* Responsible for distributing of various `NodeLifecycleEvent` to `NodeLifecycleObserver`.
*
* This class may do it in an asynchronous fashion. Also it might listen to the feedback from observers on the notifications sent and perform
* actions depending on the observer's priority.
*
* The class is safe for concurrent use from multiple threads.
*/
class NodeLifecycleEventsDistributor {
companion object {
private val log = contextLogger()
private val criticalEventsClasses: Set<Class<out NodeLifecycleEvent>> = setOf(
NodeLifecycleEvent.BeforeNodeStart::class.java,
NodeLifecycleEvent.AfterNodeStart::class.java,
NodeLifecycleEvent.StateMachineStarted::class.java)
private val criticalExceptionsClasses: Set<Class<out Throwable>> = setOf(CordaServiceCriticalFailureException::class.java)
}
/**
* Order is maintained by priority and within equal priority by full class name.
*/
private val prioritizedObservers: MutableList<NodeLifecycleObserver> = mutableListOf()
private val readWriteLock: ReadWriteLock = ReentrantReadWriteLock()
private val executor = Executors.newSingleThreadExecutor(
ThreadFactoryBuilder().setNameFormat("NodeLifecycleEventsDistributor-%d").build())
/**
* Adds observer to the distribution list.
*/
fun <T : NodeLifecycleObserver> add(observer: T) : T {
addAll(singleton(observer))
return observer
}
/**
* Adds multiple observers to the distribution list.
*/
fun <T : NodeLifecycleObserver> addAll(observers: Collection<T>) : Collection<T> {
data class SortingKey(val priority: Int, val clazz: Class<*>) : Comparable<SortingKey> {
override fun compareTo(other: SortingKey): Int {
if(priority != other.priority) {
// Reversing sorting order such that higher priorities come first
return other.priority - priority
}
// Within the same priority order alphabetically by class name to deterministic order
return clazz.name.compareTo(other.clazz.name)
}
}
readWriteLock.writeLock().executeLocked {
prioritizedObservers.addAll(observers)
// In-place sorting
prioritizedObservers.sortBy { SortingKey(it.priority, it.javaClass) }
}
return observers
}
/**
* Distributes event to all the observers previously added
*
* @return [CordaFuture] to signal when distribution is finished and delivered to all the observers
*/
fun distributeEvent(event: NodeLifecycleEvent): CordaFuture<Unit> {
val snapshot = readWriteLock.readLock().executeLocked { LinkedList(prioritizedObservers) }
val result = openFuture<Any?>()
executor.execute {
val orderedSnapshot = if (event.reversedPriority) snapshot.reversed() else snapshot
orderedSnapshot.forEach {
log.debug("Distributing event $event to: $it")
val updateResult = it.update(event)
if (updateResult.isSuccess) {
log.debug("Event $event distribution outcome: $updateResult")
} else {
log.error("Failed to distribute event $event, failure outcome: $updateResult")
handlePossibleFatalTermination(event, updateResult as Try.Failure<String>)
}
}
result.set(null)
}
return result.map { }
}
private fun handlePossibleFatalTermination(event: NodeLifecycleEvent, updateFailed: Try.Failure<String>) {
if (event.javaClass in criticalEventsClasses && updateFailed.exception.javaClass in criticalExceptionsClasses) {
log.error("During processing of $event critical failure been reported: $updateFailed. JVM will be terminated.")
exitProcess(1)
} else {
log.warn("During processing of $event non-critical failure been reported: $updateFailed.")
}
}
/**
* Custom implementation vs. using [kotlin.concurrent.withLock] to allow interruption during lock acquisition.
*/
private fun <T> Lock.executeLocked(block: () -> T) : T {
lockInterruptibly()
try {
return block()
} finally {
unlock()
}
}
}

View File

@ -0,0 +1,56 @@
package net.corda.nodeapi.internal.lifecycle
import net.corda.core.utilities.Try
/**
* Interface to flag interest in the Corda Node lifecycle which involves being notified when the node is starting up or
* shutting down.
* Unlike [net.corda.core.node.services.ServiceLifecycleObserver] this is an internal interface that provides much richer
* functionality for interacting with node's internal services.
*/
interface NodeLifecycleObserver {
companion object {
const val RPC_PRIORITY_HIGH = 1200
const val RPC_PRIORITY_NORMAL = 1100
const val RPC_PRIORITY_LOW = 1020
/**
* Helper method to create a string to flag successful processing of an event.
*/
@Suppress("unused")
inline fun <reified T : NodeLifecycleObserver> T.reportSuccess(nodeLifecycleEvent: NodeLifecycleEvent) : String =
"${T::class.java} successfully processed $nodeLifecycleEvent"
}
/**
* Used to inform `NodeLifecycleObserver` of certain `NodeLifecycleEvent`.
*
* @return If even been processed successfully and the are no error conditions `Try.Success` with brief status, otherwise `Try.Failure`
* with exception explaining what went wrong.
* It is down to subject (i.e. Node) to decide what to do in case of failure and decision may depend on the Observer's priority.
*/
fun update(nodeLifecycleEvent: NodeLifecycleEvent) : Try<String> = Try.on { "${javaClass.simpleName} ignored $nodeLifecycleEvent" }
/**
* It is possible to optionally override observer priority.
*
* `start` methods will be invoked in the ascending sequence priority order. For items with the same order alphabetical ordering
* of full class name will be applied.
* For `stop` methods, the order will be opposite to `start`.
*/
val priority: Int
}
/**
* A set of events to flag the important milestones in the lifecycle of the node.
* @param reversedPriority flags whether it would make sense to notify observers in the reversed order.
*/
sealed class NodeLifecycleEvent(val reversedPriority: Boolean = false) {
class BeforeNodeStart(val nodeInitialContext: NodeInitialContext) : NodeLifecycleEvent()
class AfterNodeStart<out T : NodeServicesContext>(val nodeServicesContext: T) : NodeLifecycleEvent()
class StateMachineStarted<out T : NodeServicesContext>(val nodeServicesContext: T) : NodeLifecycleEvent()
class StateMachineStopped<out T : NodeServicesContext>(val nodeServicesContext: T) : NodeLifecycleEvent(reversedPriority = true)
class BeforeNodeStop<out T : NodeServicesContext>(val nodeServicesContext: T) : NodeLifecycleEvent(reversedPriority = true)
class AfterNodeStop(val nodeInitialContext: NodeInitialContext) : NodeLifecycleEvent(reversedPriority = true)
}

View File

@ -0,0 +1,17 @@
package net.corda.nodeapi.internal.lifecycle
import net.corda.core.CordaInternal
import net.corda.core.serialization.SerializeAsToken
/**
* Defines a set of properties that will be available for services to perform useful activity with side effects.
*/
interface NodeServicesContext : NodeInitialContext {
/**
* Special services which upon serialisation will be represented in the stream by a special token. On the remote side
* during deserialization token will be read and corresponding instance found and wired as necessary.
*/
@CordaInternal
val tokenizableServices: List<SerializeAsToken>
}

View File

@ -0,0 +1,61 @@
package net.corda.nodeapi.internal.lifecycle
import com.nhaarman.mockito_kotlin.mock
import net.corda.core.internal.stream
import net.corda.core.utilities.Try
import net.corda.core.utilities.contextLogger
import org.junit.Test
import net.corda.nodeapi.internal.lifecycle.NodeLifecycleObserver.Companion.reportSuccess
import java.util.concurrent.atomic.AtomicLong
import kotlin.test.assertTrue
internal class NodeLifecycleEventsDistributorMultiThreadedTest {
companion object {
private val logger = contextLogger()
}
private val instance = NodeLifecycleEventsDistributor()
private val addedCounter = AtomicLong()
private val eventsDeliveredCounter = AtomicLong()
@Test
fun addAndDistributeConcurrently() {
val initialObserversCount = 10
repeat(initialObserversCount) { instance.add(MyObserver(it)) }
val operationsCount = 100_000
val event = NodeLifecycleEvent.BeforeNodeStart(mock())
val additionFreq = 1000
val distributionFutures = (1..operationsCount).stream(true).mapToObj {
if(it % additionFreq == 0) {
logger.debug("Adding observer")
instance.add(MyObserver(it))
addedCounter.incrementAndGet()
logger.info("Progress so far: $it")
}
logger.debug("Distributing event")
instance.distributeEvent(event)
}
distributionFutures.forEach { it.get() }
with(eventsDeliveredCounter.get()) {
// Greater than original observers times events
assertTrue("$this") { this > initialObserversCount.toLong() * operationsCount }
// Less than ever added observers times events
assertTrue("$this") { this < (initialObserversCount.toLong() + addedCounter.get()) * operationsCount }
}
}
inner class MyObserver(seqNum: Int) : NodeLifecycleObserver {
override val priority: Int = seqNum % 10
override fun update(nodeLifecycleEvent: NodeLifecycleEvent): Try<String> = Try.on {
eventsDeliveredCounter.incrementAndGet()
reportSuccess(nodeLifecycleEvent)
}
}
}

View File

@ -0,0 +1,64 @@
package net.corda.node.services;
import co.paralleluniverse.fibers.Suspendable;
import net.corda.core.flows.FlowLogic;
import net.corda.core.flows.StartableByRPC;
import net.corda.core.node.AppServiceHub;
import net.corda.core.node.services.CordaService;
import net.corda.core.node.services.ServiceLifecycleEvent;
import net.corda.core.serialization.SingletonSerializeAsToken;
import java.util.ArrayList;
import java.util.List;
import static net.corda.core.node.AppServiceHub.SERVICE_PRIORITY_NORMAL;
public class JavaCordaServiceLifecycle {
static final List<ServiceLifecycleEvent> eventsCaptured = new ArrayList<>();
@StartableByRPC
public static class JavaComputeTextLengthThroughCordaService extends FlowLogic<Integer> {
private final String text;
public JavaComputeTextLengthThroughCordaService(String text) {
this.text = text;
}
@Override
@Suspendable
public Integer call() {
JavaTextLengthComputingService service = getServiceHub().cordaService(JavaTextLengthComputingService.class);
return service.computeLength(text);
}
}
@CordaService
public static class JavaTextLengthComputingService extends SingletonSerializeAsToken {
private final AppServiceHub serviceHub;
public JavaTextLengthComputingService(AppServiceHub serviceHub) {
this.serviceHub = serviceHub;
serviceHub.register(SERVICE_PRIORITY_NORMAL, this::addEvent);
}
private void addEvent(ServiceLifecycleEvent event) {
switch (event) {
case STATE_MACHINE_STARTED:
eventsCaptured.add(event);
break;
default:
// Process other typed of events
break;
}
}
public int computeLength(String text) {
assert !text.isEmpty();
return text.length();
}
}
}

View File

@ -0,0 +1,129 @@
package net.corda.node.services
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.StartableByService
import net.corda.core.identity.Party
import net.corda.core.node.AppServiceHub
import net.corda.core.node.services.CordaService
import net.corda.core.node.services.ServiceLifecycleEvent
import net.corda.core.node.services.ServiceLifecycleObserver
import net.corda.core.node.services.Vault
import net.corda.core.serialization.SingletonSerializeAsToken
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.DOLLARS
import net.corda.finance.contracts.asset.Cash
import net.corda.finance.flows.AbstractCashFlow
import net.corda.finance.flows.CashIssueAndPaymentFlow
import net.corda.testing.common.internal.eventually
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.driver.DriverParameters
import net.corda.testing.driver.driver
import net.corda.testing.node.internal.FINANCE_CORDAPPS
import net.corda.testing.node.internal.enclosedCordapp
import org.junit.After
import org.junit.Test
import java.io.File
import kotlin.test.assertEquals
import kotlin.test.assertTrue
/**
* The idea of this test is upon start-up of the node check if cash been already issued and if not issue under certain reference.
* If state is already present - do nothing.
*/
class CordaServiceIssueOnceAtStartupTests {
companion object {
private val armedPropName = this::class.java.enclosingClass.name + "-armed"
private val logger = contextLogger()
private val tempFilePropertyName = this::class.java.enclosingClass.name + "-tmpFile"
private val tmpFile = createTempFile(prefix = tempFilePropertyName)
private const val vaultQueryExecutedMarker = "VaultQueryExecuted"
private const val sentFlowMarker = "SentFlow"
}
@Test
fun test() {
driver(DriverParameters(startNodesInProcess = false, cordappsForAllNodes = FINANCE_CORDAPPS + enclosedCordapp(), inMemoryDB = false,
systemProperties = mapOf(armedPropName to "true", tempFilePropertyName to tmpFile.absolutePath))) {
var node = startNode(providedName = ALICE_NAME).getOrThrow()
var page: Vault.Page<Cash.State>?
eventually(duration = 10.seconds) {
page = node.rpc.vaultQuery(Cash.State::class.java)
assertTrue(page!!.states.isNotEmpty())
assertEquals(listOf(vaultQueryExecutedMarker, sentFlowMarker), tmpFile.readLines(), "First start tracker")
}
node.stop()
node = startNode(providedName = ALICE_NAME).getOrThrow()
eventually(duration = 10.seconds) {
assertEquals(listOf(vaultQueryExecutedMarker, sentFlowMarker, vaultQueryExecutedMarker),
tmpFile.readLines(), "Re-start tracker")
}
}
}
@After
fun testDown() {
System.clearProperty(armedPropName)
tmpFile.delete()
}
@CordaService
@Suppress("unused")
class IssueAndPayOnceService(private val services: AppServiceHub) : SingletonSerializeAsToken() {
init {
// There are some "greedy" tests that may take on the package of this test and include it into the CorDapp they assemble
// Without the "secret" property service upon instantiation will be subscribed to lifecycle events which would be unwanted.
// Also do not do this for Notary
val myName = services.myInfo.legalIdentities.single().name
val notaryName = services.networkMapCache.notaryIdentities.single().name
if(java.lang.Boolean.getBoolean(armedPropName) && myName != notaryName) {
services.register(observer = MyServiceLifecycleObserver())
} else {
logger.info("Skipping lifecycle events registration for $myName")
}
}
inner class MyServiceLifecycleObserver : ServiceLifecycleObserver {
override fun onServiceLifecycleEvent(event: ServiceLifecycleEvent) {
val tmpFile = File(System.getProperty(tempFilePropertyName))
if (event == ServiceLifecycleEvent.STATE_MACHINE_STARTED) {
val queryResult = services.vaultService.queryBy(Cash.State::class.java)
if (tmpFile.length() == 0L) {
tmpFile.appendText(vaultQueryExecutedMarker)
} else {
tmpFile.appendText("\n" + vaultQueryExecutedMarker)
}
if(queryResult.states.isEmpty()) {
val issueAndPayResult = services.startFlow(
IssueAndPayByServiceFlow(
services.myInfo.legalIdentities.single(), services.networkMapCache.notaryIdentities.single()))
.returnValue.getOrThrow()
logger.info("Cash issued and paid: $issueAndPayResult")
tmpFile.appendText("\n" + sentFlowMarker)
}
}
}
}
}
/**
* The only purpose to have this is to be able to have annotation: [StartableByService]
*/
@StartableByService
class IssueAndPayByServiceFlow(private val recipient: Party, private val notary: Party) : FlowLogic<AbstractCashFlow.Result>() {
@Suspendable
override fun call(): AbstractCashFlow.Result {
return subFlow(CashIssueAndPaymentFlow(500.DOLLARS,
OpaqueBytes.of(0x01),
recipient,
false,
notary))
}
}
}

View File

@ -0,0 +1,65 @@
package net.corda.node.services
import net.corda.core.node.AppServiceHub
import net.corda.core.node.services.CordaService
import net.corda.core.node.services.CordaServiceCriticalFailureException
import net.corda.core.node.services.ServiceLifecycleEvent
import net.corda.core.node.services.ServiceLifecycleObserver
import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.utilities.getOrThrow
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.driver.DriverParameters
import net.corda.testing.driver.driver
import net.corda.testing.node.internal.ListenProcessDeathException
import net.corda.testing.node.internal.enclosedCordapp
import org.junit.Test
import kotlin.test.assertFailsWith
class CordaServiceLifecycleFatalTests {
companion object {
// It is important to disarm throwing of the exception as unfortunately this service may be packaged to many
// test cordaps, e.g. the one used by [net.corda.node.CordappScanningDriverTest]
// If service remains "armed" to throw exceptions this will fail node start-up sequence.
// The problem is caused by the fact that test from `net.corda.node` package also hoovers all the sub-packages.
// Since this is done as a separate process, the trigger is passed through the system property.
const val SECRET_PROPERTY_NAME = "CordaServiceLifecycleFatalTests.armed"
@CordaService
@Suppress("unused")
class FatalService(services: AppServiceHub) : SingletonSerializeAsToken() {
init {
services.register(observer = FailingObserver)
}
fun computeLength(text: String): Int {
require(text.isNotEmpty()) { "Length must be at least 1." }
return text.length
}
}
object FailingObserver : ServiceLifecycleObserver {
override fun onServiceLifecycleEvent(event: ServiceLifecycleEvent) {
if(java.lang.Boolean.getBoolean(SECRET_PROPERTY_NAME)) {
throw CordaServiceCriticalFailureException("failure")
}
}
}
}
@Test
fun `JVM terminates on critical failure`() {
// Scenario terminates JVM - node should be running out of process
driver(DriverParameters(startNodesInProcess = false, cordappsForAllNodes = listOf(enclosedCordapp()),
notarySpecs = emptyList(), systemProperties = mapOf(Pair(SECRET_PROPERTY_NAME, "true")))) {
val nodeHandle = startNode(providedName = ALICE_NAME)
// Ensure ample time for all the stat-up lifecycle events to be processed
Thread.sleep(2000)
assertFailsWith(ListenProcessDeathException::class) {
nodeHandle.getOrThrow()
}
}
}
}

View File

@ -0,0 +1,74 @@
package net.corda.node.services
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.node.AppServiceHub
import net.corda.core.node.services.CordaService
import net.corda.core.node.services.ServiceLifecycleEvent
import net.corda.core.node.services.ServiceLifecycleEvent.STATE_MACHINE_STARTED
import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.utilities.getOrThrow
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.driver.DriverParameters
import net.corda.testing.driver.driver
import net.corda.testing.node.internal.enclosedCordapp
import org.junit.Test
import kotlin.test.assertEquals
class CordaServiceLifecycleTests {
private companion object {
const val TEST_PHRASE = "testPhrase"
private val eventsCaptured: MutableList<ServiceLifecycleEvent> = mutableListOf()
}
@Test
fun `corda service receives events`() {
eventsCaptured.clear()
val result = driver(DriverParameters(startNodesInProcess = true, cordappsForAllNodes = listOf(enclosedCordapp()),
notarySpecs = emptyList())) {
val node = startNode(providedName = ALICE_NAME).getOrThrow()
node.rpc.startFlow(::ComputeTextLengthThroughCordaService, TEST_PHRASE).returnValue.getOrThrow()
}
assertEquals(TEST_PHRASE.length, result)
assertEquals(1, eventsCaptured.size)
assertEquals(listOf(STATE_MACHINE_STARTED), eventsCaptured)
}
@StartableByRPC
class ComputeTextLengthThroughCordaService(private val text: String) : FlowLogic<Int>() {
@Suspendable
override fun call(): Int {
val service = serviceHub.cordaService(TextLengthComputingService::class.java)
return service.computeLength(text)
}
}
@CordaService
@Suppress("unused")
class TextLengthComputingService(services: AppServiceHub) : SingletonSerializeAsToken() {
init {
services.register { addEvent(it) }
}
private fun addEvent(event: ServiceLifecycleEvent) {
when (event) {
STATE_MACHINE_STARTED -> {
eventsCaptured.add(event)
}
else -> {
eventsCaptured.add(event)
}
}
}
fun computeLength(text: String): Int {
require(text.isNotEmpty()) { "Length must be at least 1." }
return text.length
}
}
}

View File

@ -0,0 +1,31 @@
package net.corda.node.services
import net.corda.core.messaging.startFlow
import net.corda.core.node.services.ServiceLifecycleEvent
import net.corda.core.utilities.getOrThrow
import net.corda.node.services.JavaCordaServiceLifecycle.JavaComputeTextLengthThroughCordaService
import net.corda.node.services.JavaCordaServiceLifecycle.eventsCaptured
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.driver.DriverParameters
import net.corda.testing.driver.driver
import org.junit.Test
import kotlin.test.assertEquals
class JavaCordaServiceLifecycleTests {
private companion object {
const val TEST_PHRASE = "javaTestPhrase"
}
@Test
fun `corda service receives events`() {
eventsCaptured.clear()
val result = driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) {
val node = startNode(providedName = ALICE_NAME).getOrThrow()
node.rpc.startFlow(::JavaComputeTextLengthThroughCordaService, TEST_PHRASE).returnValue.getOrThrow()
}
assertEquals(TEST_PHRASE.length, result)
assertEquals(1, eventsCaptured.size)
assertEquals(listOf(ServiceLifecycleEvent.STATE_MACHINE_STARTED), eventsCaptured)
}
}

View File

@ -19,7 +19,6 @@ import net.corda.core.flows.FlowLogicRefFactory
import net.corda.core.flows.InitiatedBy
import net.corda.core.flows.NotaryChangeFlow
import net.corda.core.flows.NotaryFlow
import net.corda.core.flows.StartableByService
import net.corda.core.flows.StateMachineRunId
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.CordaX500Name
@ -31,7 +30,7 @@ import net.corda.core.internal.NODE_INFO_DIRECTORY
import net.corda.core.internal.NamedCacheFactory
import net.corda.core.internal.NetworkParametersStorage
import net.corda.core.internal.VisibleForTesting
import net.corda.core.internal.concurrent.doneFuture
import net.corda.core.internal.concurrent.flatMap
import net.corda.core.internal.concurrent.map
import net.corda.core.internal.concurrent.openFuture
import net.corda.core.internal.div
@ -41,10 +40,6 @@ import net.corda.core.internal.rootMessage
import net.corda.core.internal.uncheckedCast
import net.corda.core.messaging.ClientRpcSslOptions
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.messaging.FlowHandle
import net.corda.core.messaging.FlowHandleImpl
import net.corda.core.messaging.FlowProgressHandle
import net.corda.core.messaging.FlowProgressHandleImpl
import net.corda.core.messaging.RPCOps
import net.corda.core.node.AppServiceHub
import net.corda.core.node.NetworkParameters
@ -57,7 +52,6 @@ import net.corda.core.node.services.diagnostics.DiagnosticsService
import net.corda.core.node.services.IdentityService
import net.corda.core.node.services.KeyManagementService
import net.corda.core.node.services.TransactionVerifierService
import net.corda.core.node.services.vault.CordaTransactionSupport
import net.corda.core.schemas.MappedSchema
import net.corda.core.serialization.SerializationWhitelist
import net.corda.core.serialization.SerializeAsToken
@ -65,11 +59,12 @@ import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.transactions.LedgerTransaction
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.days
import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.minutes
import net.corda.nodeapi.internal.lifecycle.NodeServicesContext
import net.corda.djvm.source.ApiSource
import net.corda.djvm.source.EmptyApi
import net.corda.djvm.source.UserSource
import net.corda.nodeapi.internal.lifecycle.NodeLifecycleEvent
import net.corda.node.CordaClock
import net.corda.node.VersionInfo
import net.corda.node.internal.classloading.requireAnnotation
@ -89,6 +84,7 @@ import net.corda.node.services.api.FlowStarter
import net.corda.node.services.api.MonitoringService
import net.corda.node.services.api.NetworkMapCacheInternal
import net.corda.node.services.api.NodePropertiesStore
import net.corda.nodeapi.internal.lifecycle.NodeLifecycleEventsDistributor
import net.corda.node.services.api.SchemaService
import net.corda.node.services.api.ServiceHubInternal
import net.corda.node.services.api.VaultServiceInternal
@ -122,7 +118,7 @@ import net.corda.node.services.persistence.NodeAttachmentService
import net.corda.node.services.persistence.NodePropertiesPersistentStore
import net.corda.node.services.persistence.PublicKeyToOwningIdentityCacheImpl
import net.corda.node.services.persistence.PublicKeyToTextConverter
import net.corda.node.services.rpc.CheckpointDumper
import net.corda.node.services.rpc.CheckpointDumperImpl
import net.corda.node.services.schema.NodeSchemaService
import net.corda.node.services.statemachine.Event
import net.corda.node.services.statemachine.ExternalEvent
@ -170,7 +166,6 @@ import org.apache.activemq.artemis.utils.ReusableLatch
import org.jolokia.jvmagent.JolokiaServer
import org.jolokia.jvmagent.JolokiaServerConfig
import org.slf4j.Logger
import rx.Observable
import rx.Scheduler
import java.io.IOException
import java.lang.reflect.InvocationTargetException
@ -182,7 +177,6 @@ import java.sql.Connection
import java.time.Clock
import java.time.Duration
import java.time.format.DateTimeParseException
import java.util.Objects
import java.util.Properties
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
@ -212,7 +206,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
protected abstract val log: Logger
@Suppress("LeakingThis")
private var tokenizableServices: MutableList<Any>? = mutableListOf(platformClock, this)
private var tokenizableServices: MutableList<SerializeAsToken>? = mutableListOf(platformClock, this)
val metricRegistry = MetricRegistry()
protected val cacheFactory = cacheFactoryPrototype.bindWithConfig(configuration).bindWithMetrics(metricRegistry).tokenize()
@ -244,7 +238,9 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
schemaService,
configuration.dataSourceProperties,
cacheFactory,
this.cordappLoader.appClassLoader)
cordappLoader.appClassLoader)
private val transactionSupport = CordaTransactionSupportImpl(database)
init {
// TODO Break cyclic dependency
@ -358,8 +354,22 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
@Volatile
private var _started: S? = null
private val checkpointDumper = CheckpointDumperImpl(checkpointStorage, database, services, services.configuration.baseDirectory)
private val nodeServicesContext = object : NodeServicesContext {
override val platformVersion = versionInfo.platformVersion
override val configurationWithOptions = configuration.configurationWithOptions
// Note: tokenizableServices passed by reference meaning that any subsequent modification to the content in the `AbstractNode` will
// be reflected in the context as well. However, since context only has access to immutable collection it can only read (but not modify)
// the content.
override val tokenizableServices: List<SerializeAsToken> = this@AbstractNode.tokenizableServices!!
}
private val nodeLifecycleEventsDistributor = NodeLifecycleEventsDistributor().apply { add(checkpointDumper) }
private fun <T : Any> T.tokenize(): T {
tokenizableServices?.add(this)
tokenizableServices?.add(this as? SerializeAsToken ?:
throw IllegalStateException("${this::class.java} is expected to be extending from SerializeAsToken"))
?: throw IllegalStateException("The tokenisable services list has already been finalised")
return this
}
@ -370,7 +380,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
}
/** The implementation of the [CordaRPCOps] interface used by this node. */
open fun makeRPCOps(cordappLoader: CordappLoader, checkpointDumper: CheckpointDumper): CordaRPCOps {
open fun makeRPCOps(cordappLoader: CordappLoader, checkpointDumper: CheckpointDumperImpl): CordaRPCOps {
val ops: InternalCordaRPCOps = CordaRPCOpsImpl(
services,
smm,
@ -429,6 +439,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
if (configuration.devMode && System.getProperty("co.paralleluniverse.fibers.verifyInstrumentation") == null) {
System.setProperty("co.paralleluniverse.fibers.verifyInstrumentation", "true")
}
nodeLifecycleEventsDistributor.distributeEvent(NodeLifecycleEvent.BeforeNodeStart(nodeServicesContext))
log.info("Node starting up ...")
val trustRoot = initKeyStores()
@ -443,7 +454,6 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
installCoreFlows()
registerCordappFlows()
services.rpcFlows += cordappLoader.cordapps.flatMap { it.rpcFlows }
val checkpointDumper = CheckpointDumper(checkpointStorage, database, services, services.configuration.baseDirectory)
val rpcOps = makeRPCOps(cordappLoader, checkpointDumper)
startShell()
networkMapClient?.start(trustRoot)
@ -485,7 +495,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
}
// Do all of this in a database transaction so anything that might need a connection has one.
return database.transaction(recoverableFailureTolerance = 0) {
val (resultingNodeInfo, readyFuture) = database.transaction(recoverableFailureTolerance = 0) {
networkParametersStorage.setCurrentParameters(signedNetParams, trustRoot)
identityService.loadIdentities(nodeInfo.legalIdentitiesAndCerts)
attachments.start()
@ -505,21 +515,33 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
tokenizableServices = null
verifyCheckpointsCompatible(frozenTokenizableServices)
checkpointDumper.start(frozenTokenizableServices)
smm.start(frozenTokenizableServices)
val smmStartedFuture = smm.start(frozenTokenizableServices)
// Shut down the SMM so no Fibers are scheduled.
runOnStop += { smm.stop(acceptableLiveFiberCountOnStop()) }
val flowMonitor = FlowMonitor(
smm,
configuration.flowMonitorPeriodMillis,
configuration.flowMonitorSuspensionLoggingThresholdMillis
smm,
configuration.flowMonitorPeriodMillis,
configuration.flowMonitorSuspensionLoggingThresholdMillis
)
runOnStop += flowMonitor::stop
flowMonitor.start()
schedulerService.start()
createStartedNode(nodeInfo, rpcOps, notaryService).also { _started = it }
val resultingNodeInfo = createStartedNode(nodeInfo, rpcOps, notaryService).also { _started = it }
val readyFuture = smmStartedFuture.flatMap {
log.debug("SMM ready")
network.ready
}
resultingNodeInfo to readyFuture
}
readyFuture.map {
// NB: Dispatch lifecycle events outside of transaction to ensure attachments and the like persisted into the DB
log.debug("Distributing events")
nodeLifecycleEventsDistributor.distributeEvent(NodeLifecycleEvent.AfterNodeStart(nodeServicesContext))
nodeLifecycleEventsDistributor.distributeEvent(NodeLifecycleEvent.StateMachineStarted(nodeServicesContext))
}
return resultingNodeInfo
}
/** Subclasses must override this to create a "started" node of the desired type, using the provided machinery. */
@ -747,60 +769,11 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
}
}
/**
* This customizes the ServiceHub for each CordaService that is initiating flows.
*/
// TODO Move this into its own file
private class AppServiceHubImpl<T : SerializeAsToken>(private val serviceHub: ServiceHub, private val flowStarter: FlowStarter,
override val database: CordaTransactionSupport) : AppServiceHub, ServiceHub by serviceHub {
lateinit var serviceInstance: T
override fun <T> startTrackedFlow(flow: FlowLogic<T>): FlowProgressHandle<T> {
val stateMachine = startFlowChecked(flow)
return FlowProgressHandleImpl(
id = stateMachine.id,
returnValue = stateMachine.resultFuture,
progress = stateMachine.logic.track()?.updates ?: Observable.empty()
)
}
override fun <T> startFlow(flow: FlowLogic<T>): FlowHandle<T> {
val parentFlow = FlowLogic.currentTopLevel
return if (parentFlow != null) {
val result = parentFlow.subFlow(flow)
// Accessing the flow id must happen after the flow has started.
val flowId = flow.runId
FlowHandleImpl(flowId, doneFuture(result))
} else {
val stateMachine = startFlowChecked(flow)
FlowHandleImpl(id = stateMachine.id, returnValue = stateMachine.resultFuture)
}
}
private fun <T> startFlowChecked(flow: FlowLogic<T>): FlowStateMachine<T> {
val logicType = flow.javaClass
require(logicType.isAnnotationPresent(StartableByService::class.java)) { "${logicType.name} was not designed for starting by a CordaService" }
// TODO check service permissions
// TODO switch from myInfo.legalIdentities[0].name to current node's identity as soon as available
val context = InvocationContext.service(serviceInstance.javaClass.name, myInfo.legalIdentities[0].name)
return flowStarter.startFlow(flow, context).getOrThrow()
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is AppServiceHubImpl<*>) return false
return serviceHub == other.serviceHub
&& flowStarter == other.flowStarter
&& serviceInstance == other.serviceInstance
}
override fun hashCode() = Objects.hash(serviceHub, flowStarter, serviceInstance)
}
fun <T : SerializeAsToken> installCordaService(serviceClass: Class<T>): T {
serviceClass.requireAnnotation<CordaService>()
val service = try {
val serviceContext = AppServiceHubImpl<T>(services, flowStarter, CordaTransactionSupportImpl(database))
val serviceContext = AppServiceHubImpl<T>(services, flowStarter, transactionSupport, nodeLifecycleEventsDistributor)
val extendedServiceConstructor = serviceClass.getDeclaredConstructor(AppServiceHub::class.java).apply { isAccessible = true }
val service = extendedServiceConstructor.newInstance(serviceContext)
serviceContext.serviceInstance = service
@ -950,6 +923,10 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
}
open fun stop() {
nodeLifecycleEventsDistributor.distributeEvent(NodeLifecycleEvent.StateMachineStopped(nodeServicesContext))
nodeLifecycleEventsDistributor.distributeEvent(NodeLifecycleEvent.BeforeNodeStop(nodeServicesContext))
// TODO: We need a good way of handling "nice to have" shutdown events, especially those that deal with the
// network, including unsubscribing from updates from remote services. Possibly some sort of parameter to stop()
// to indicate "Please shut down gracefully" vs "Shut down now".
@ -963,6 +940,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
runOnStop.clear()
shutdownExecutor.shutdown()
_started = null
nodeLifecycleEventsDistributor.distributeEvent(NodeLifecycleEvent.AfterNodeStop(nodeServicesContext))
}
protected abstract fun makeMessagingService(): MessagingService

View File

@ -0,0 +1,124 @@
package net.corda.node.internal
import net.corda.core.context.InvocationContext
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.StartableByService
import net.corda.core.internal.FlowStateMachine
import net.corda.core.internal.concurrent.doneFuture
import net.corda.core.messaging.FlowHandle
import net.corda.core.messaging.FlowHandleImpl
import net.corda.core.messaging.FlowProgressHandle
import net.corda.core.messaging.FlowProgressHandleImpl
import net.corda.core.node.AppServiceHub
import net.corda.core.node.ServiceHub
import net.corda.core.node.services.ServiceLifecycleEvent
import net.corda.core.node.services.ServiceLifecycleObserver
import net.corda.core.node.services.vault.CordaTransactionSupport
import net.corda.core.serialization.SerializeAsToken
import net.corda.core.utilities.Try
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.getOrThrow
import net.corda.node.services.api.FlowStarter
import net.corda.nodeapi.internal.lifecycle.NodeLifecycleEvent
import net.corda.nodeapi.internal.lifecycle.NodeLifecycleEventsDistributor
import net.corda.nodeapi.internal.lifecycle.NodeLifecycleObserver
import net.corda.nodeapi.internal.lifecycle.NodeLifecycleObserver.Companion.reportSuccess
import rx.Observable
import java.util.*
/**
* This customizes the ServiceHub for each [net.corda.core.node.services.CordaService] that is initiating flows.
*/
internal class AppServiceHubImpl<T : SerializeAsToken>(private val serviceHub: ServiceHub, private val flowStarter: FlowStarter,
override val database: CordaTransactionSupport,
private val nodeLifecycleEventsDistributor: NodeLifecycleEventsDistributor)
: AppServiceHub, ServiceHub by serviceHub {
companion object {
private val logger = contextLogger()
private class NodeLifecycleServiceObserverAdaptor(private val observer: ServiceLifecycleObserver, override val priority: Int) : NodeLifecycleObserver {
override fun update(nodeLifecycleEvent: NodeLifecycleEvent): Try<String> {
return when(nodeLifecycleEvent) {
is NodeLifecycleEvent.StateMachineStarted<*> -> Try.on {
observer.onServiceLifecycleEvent(ServiceLifecycleEvent.STATE_MACHINE_STARTED)
reportSuccess(nodeLifecycleEvent)
}
else -> super.update(nodeLifecycleEvent)
}
}
}
}
lateinit var serviceInstance: T
@Volatile
private var flowsAllowed = false
init {
nodeLifecycleEventsDistributor.add(object : NodeLifecycleObserver {
override val priority: Int = AppServiceHub.SERVICE_PRIORITY_HIGH
override fun update(nodeLifecycleEvent: NodeLifecycleEvent): Try<String> {
return when(nodeLifecycleEvent) {
is NodeLifecycleEvent.StateMachineStarted<*> -> Try.on {
flowsAllowed = true
reportSuccess(nodeLifecycleEvent)
}
else -> super.update(nodeLifecycleEvent)
}
}
})
}
override fun <T> startTrackedFlow(flow: FlowLogic<T>): FlowProgressHandle<T> {
val stateMachine = startFlowChecked(flow)
return FlowProgressHandleImpl(
id = stateMachine.id,
returnValue = stateMachine.resultFuture,
progress = stateMachine.logic.track()?.updates ?: Observable.empty()
)
}
override fun <T> startFlow(flow: FlowLogic<T>): FlowHandle<T> {
val parentFlow = FlowLogic.currentTopLevel
return if (parentFlow != null) {
val result = parentFlow.subFlow(flow)
// Accessing the flow id must happen after the flow has started.
val flowId = flow.runId
FlowHandleImpl(flowId, doneFuture(result))
} else {
val stateMachine = startFlowChecked(flow)
FlowHandleImpl(id = stateMachine.id, returnValue = stateMachine.resultFuture)
}
}
private fun <T> startFlowChecked(flow: FlowLogic<T>): FlowStateMachine<T> {
val logicType = flow.javaClass
require(logicType.isAnnotationPresent(StartableByService::class.java)) { "${logicType.name} was not designed for starting by a CordaService" }
// TODO check service permissions
// TODO switch from myInfo.legalIdentities[0].name to current node's identity as soon as available
if(!flowsAllowed) {
logger.warn("Flow $flow started too early in the node's lifecycle, SMM may not be ready yet. " +
"Please consider registering your service to node's lifecycle event: `STATE_MACHINE_STARTED`")
}
val context = InvocationContext.service(serviceInstance.javaClass.name, myInfo.legalIdentities[0].name)
return flowStarter.startFlow(flow, context).getOrThrow()
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is AppServiceHubImpl<*>) return false
return serviceHub == other.serviceHub
&& flowStarter == other.flowStarter
&& serviceInstance == other.serviceInstance
}
override fun hashCode() = Objects.hash(serviceHub, flowStarter, serviceInstance)
override fun register(priority: Int, observer: ServiceLifecycleObserver) {
nodeLifecycleEventsDistributor.add(NodeLifecycleServiceObserverAdaptor(observer, priority))
}
}

View File

@ -51,7 +51,7 @@ 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.rpc.CheckpointDumper
import net.corda.node.services.rpc.CheckpointDumperImpl
import net.corda.node.services.rpc.context
import net.corda.node.services.statemachine.StateMachineManager
import net.corda.nodeapi.exceptions.NonRpcFlowException
@ -72,7 +72,7 @@ internal class CordaRPCOpsImpl(
private val services: ServiceHubInternal,
private val smm: StateMachineManager,
private val flowStarter: FlowStarter,
private val checkpointDumper: CheckpointDumper,
private val checkpointDumper: CheckpointDumperImpl,
private val shutdownNode: () -> Unit
) : InternalCordaRPCOps, AutoCloseable {
@ -154,7 +154,7 @@ internal class CordaRPCOpsImpl(
return services.validatedTransactions.track()
}
override fun dumpCheckpoints() = checkpointDumper.dump()
override fun dumpCheckpoints() = checkpointDumper.dumpCheckpoints()
override val attachmentTrustInfos: List<AttachmentTrustInfo>
get() {

View File

@ -633,6 +633,7 @@ open class Node(configuration: NodeConfiguration,
/** Starts a blocking event loop for message dispatch. */
fun run() {
internalRpcMessagingClient?.start(rpcBroker!!.serverControl)
printBasicNodeInfo("Running P2PMessaging loop")
(network as P2PMessagingClient).run()
}

View File

@ -2,6 +2,7 @@ package net.corda.node.services.config
import com.typesafe.config.Config
import net.corda.common.configuration.parsing.internal.Configuration
import net.corda.common.configuration.parsing.internal.ConfigurationWithOptionsContainer
import net.corda.common.validation.internal.Validated
import net.corda.core.context.AuthServiceId
import net.corda.core.identity.CordaX500Name
@ -25,7 +26,7 @@ import javax.security.auth.x500.X500Principal
val Int.MB: Long get() = this * 1024L * 1024L
interface NodeConfiguration {
interface NodeConfiguration : ConfigurationWithOptionsContainer {
val myLegalName: CordaX500Name
val emailAddress: String
val jmxMonitoringHttpPort: Int?

View File

@ -1,6 +1,7 @@
package net.corda.node.services.config
import com.typesafe.config.ConfigException
import net.corda.common.configuration.parsing.internal.ConfigurationWithOptions
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.div
import net.corda.core.utilities.NetworkHostAndPort
@ -79,7 +80,8 @@ data class NodeConfigurationImpl(
override val cordappSignerKeyFingerprintBlacklist: List<String> = Defaults.cordappSignerKeyFingerprintBlacklist,
override val networkParameterAcceptanceSettings: NetworkParameterAcceptanceSettings? =
Defaults.networkParameterAcceptanceSettings,
override val blacklistedAttachmentSigningKeys: List<String> = Defaults.blacklistedAttachmentSigningKeys
override val blacklistedAttachmentSigningKeys: List<String> = Defaults.blacklistedAttachmentSigningKeys,
override val configurationWithOptions: ConfigurationWithOptions
) : NodeConfiguration {
internal object Defaults {
val jmxMonitoringHttpPort: Int? = null

View File

@ -125,7 +125,8 @@ internal object V1NodeConfigurationSpec : Configuration.Specification<NodeConfig
cordappDirectories = cordappDirectories.map { baseDirectoryPath.resolve(it) },
cordappSignerKeyFingerprintBlacklist = configuration[cordappSignerKeyFingerprintBlacklist],
blacklistedAttachmentSigningKeys = configuration[blacklistedAttachmentSigningKeys],
networkParameterAcceptanceSettings = configuration[networkParameterAcceptanceSettings]
networkParameterAcceptanceSettings = configuration[networkParameterAcceptanceSettings],
configurationWithOptions = ConfigurationWithOptions(configuration, Configuration.Validation.Options.defaults)
))
} catch (e: Exception) {
return when (e) {

View File

@ -1,6 +1,7 @@
package net.corda.node.services.messaging
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.concurrent.CordaFuture
import net.corda.core.crypto.newSecureRandom
import net.corda.core.identity.CordaX500Name
import net.corda.core.messaging.MessageRecipients
@ -106,6 +107,11 @@ interface MessagingService : AutoCloseable {
/** Returns an address that refers to this node. */
val myAddress: SingleMessageRecipient
/**
* Signals when ready and fully operational
*/
val ready: CordaFuture<Void?>
}
fun MessagingService.send(topicSession: String, payload: Any, to: MessageRecipients, deduplicationId: SenderDeduplicationId = SenderDeduplicationId(DeduplicationId.createRandom(newSecureRandom()), ourSenderUUID), additionalHeaders: Map<String, String> = emptyMap()) = send(createMessage(topicSession, payload.serialize().bytes, deduplicationId, additionalHeaders), to)

View File

@ -7,6 +7,8 @@ import net.corda.core.flows.StateMachineRunId
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.NamedCacheFactory
import net.corda.core.internal.ThreadBox
import net.corda.core.internal.concurrent.OpenFuture
import net.corda.core.internal.concurrent.openFuture
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.messaging.MessageRecipients
import net.corda.core.messaging.SingleMessageRecipient
@ -143,6 +145,8 @@ class P2PMessagingClient(val config: NodeConfiguration,
private val deduplicator = P2PMessageDeduplicator(cacheFactory, database)
internal var messagingExecutor: MessagingExecutor? = null
override val ready: OpenFuture<Void?> = openFuture()
/**
* @param myIdentity The primary identity of the node, which defines the messaging address for externally received messages.
* It is also used to construct the myAddress field, which is ultimately advertised in the network map.
@ -351,6 +355,9 @@ class P2PMessagingClient(val config: NodeConfiguration,
p2pConsumer!!
}
consumer.start()
log.debug("Signalling ready")
ready.set(null)
log.debug("Awaiting on latch")
latch.await()
} finally {
shutdownLatch.countDown()

View File

@ -28,6 +28,7 @@ import net.corda.core.flows.StateMachineRunId
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.internal.*
import net.corda.core.node.AppServiceHub.Companion.SERVICE_PRIORITY_NORMAL
import net.corda.core.node.ServiceHub
import net.corda.core.serialization.SerializeAsToken
import net.corda.core.serialization.SerializedBytes
@ -37,7 +38,11 @@ import net.corda.core.serialization.internal.CheckpointSerializationDefaults
import net.corda.core.serialization.internal.checkpointDeserialize
import net.corda.core.utilities.NonEmptySet
import net.corda.core.utilities.ProgressTracker
import net.corda.core.utilities.Try
import net.corda.core.utilities.contextLogger
import net.corda.nodeapi.internal.lifecycle.NodeLifecycleEvent
import net.corda.nodeapi.internal.lifecycle.NodeLifecycleObserver
import net.corda.nodeapi.internal.lifecycle.NodeLifecycleObserver.Companion.reportSuccess
import net.corda.node.internal.NodeStartup
import net.corda.node.services.api.CheckpointStorage
import net.corda.node.services.statemachine.*
@ -56,12 +61,15 @@ import java.util.concurrent.atomic.AtomicInteger
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
class CheckpointDumper(private val checkpointStorage: CheckpointStorage, private val database: CordaPersistence, private val serviceHub: ServiceHub, val baseDirectory: Path) {
class CheckpointDumperImpl(private val checkpointStorage: CheckpointStorage, private val database: CordaPersistence,
private val serviceHub: ServiceHub, val baseDirectory: Path) : NodeLifecycleObserver {
companion object {
internal val TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss").withZone(UTC)
private val log = contextLogger()
}
override val priority: Int = SERVICE_PRIORITY_NORMAL
private val lock = AtomicInteger(0)
private lateinit var checkpointSerializationContext: CheckpointSerializationContext
@ -71,32 +79,38 @@ class CheckpointDumper(private val checkpointStorage: CheckpointStorage, private
checkpointAgentRunning()
}
fun start(tokenizableServices: List<Any>) {
checkpointSerializationContext = CheckpointSerializationDefaults.CHECKPOINT_CONTEXT.withTokenContext(
CheckpointSerializeAsTokenContextImpl(
tokenizableServices,
CheckpointSerializationDefaults.CHECKPOINT_SERIALIZER,
CheckpointSerializationDefaults.CHECKPOINT_CONTEXT,
serviceHub
override fun update(nodeLifecycleEvent: NodeLifecycleEvent): Try<String> {
return when(nodeLifecycleEvent) {
is NodeLifecycleEvent.AfterNodeStart<*> -> Try.on {
checkpointSerializationContext = CheckpointSerializationDefaults.CHECKPOINT_CONTEXT.withTokenContext(
CheckpointSerializeAsTokenContextImpl(
nodeLifecycleEvent.nodeServicesContext.tokenizableServices,
CheckpointSerializationDefaults.CHECKPOINT_SERIALIZER,
CheckpointSerializationDefaults.CHECKPOINT_CONTEXT,
serviceHub
)
)
)
val mapper = JacksonSupport.createNonRpcMapper()
mapper.registerModule(SimpleModule().apply {
setSerializerModifier(CheckpointDumperBeanModifier)
addSerializer(FlowSessionImplSerializer)
addSerializer(MapSerializer)
addSerializer(AttachmentSerializer)
setMixInAnnotation(FlowLogic::class.java, FlowLogicMixin::class.java)
setMixInAnnotation(SessionId::class.java, SessionIdMixin::class.java)
})
val prettyPrinter = DefaultPrettyPrinter().apply {
indentArraysWith(DefaultIndenter.SYSTEM_LINEFEED_INSTANCE)
val mapper = JacksonSupport.createNonRpcMapper()
mapper.registerModule(SimpleModule().apply {
setSerializerModifier(CheckpointDumperBeanModifier)
addSerializer(FlowSessionImplSerializer)
addSerializer(MapSerializer)
addSerializer(AttachmentSerializer)
setMixInAnnotation(FlowLogic::class.java, FlowLogicMixin::class.java)
setMixInAnnotation(SessionId::class.java, SessionIdMixin::class.java)
})
val prettyPrinter = DefaultPrettyPrinter().apply {
indentArraysWith(DefaultIndenter.SYSTEM_LINEFEED_INSTANCE)
}
writer = mapper.writer(prettyPrinter)
reportSuccess(nodeLifecycleEvent)
}
else -> super.update(nodeLifecycleEvent)
}
writer = mapper.writer(prettyPrinter)
}
fun dump() {
fun dumpCheckpoints() {
val now = serviceHub.clock.instant()
val file = baseDirectory / NodeStartup.LOGS_DIRECTORY_NAME / "checkpoints_dump-${TIME_FORMATTER.format(now)}.zip"
try {
@ -393,7 +407,7 @@ class CheckpointDumper(private val checkpointStorage: CheckpointStorage, private
private object MapSerializer : JsonSerializer<Map<Any, Any>>() {
override fun serialize(map: Map<Any, Any>, gen: JsonGenerator, serializers: SerializerProvider) {
gen.writeStartArray(map.size)
map.forEach { key, value ->
map.forEach { (key, value) ->
gen.jsonObject {
writeObjectField("key", key)
writeObjectField("value", value)
@ -404,4 +418,4 @@ class CheckpointDumper(private val checkpointStorage: CheckpointStorage, private
override fun handledType(): Class<Map<Any, Any>> = uncheckedCast(Map::class.java)
}
}
}

View File

@ -131,7 +131,7 @@ class SingleThreadedStateMachineManager(
*/
override val changes: Observable<StateMachineManager.Change> = mutex.content.changesPublisher
override fun start(tokenizableServices: List<Any>) {
override fun start(tokenizableServices: List<Any>) : CordaFuture<Unit> {
checkQuasarJavaAgentPresence()
val checkpointSerializationContext = CheckpointSerializationDefaults.CHECKPOINT_CONTEXT.withTokenContext(
CheckpointSerializeAsTokenContextImpl(
@ -153,7 +153,7 @@ class SingleThreadedStateMachineManager(
(fiber as FlowStateMachineImpl<*>).logger.warn("Caught exception from flow", throwable)
}
}
serviceHub.networkMapCache.nodeReady.then {
return serviceHub.networkMapCache.nodeReady.map {
logger.info("Node ready, info: ${serviceHub.myInfo}")
resumeRestoredFlows(fibers)
flowMessaging.start { _, deduplicationHandler ->

View File

@ -10,6 +10,7 @@ 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.util.concurrent.Future
/**
* A StateMachineManager is responsible for coordination and persistence of multiple [FlowStateMachine] objects.
@ -31,8 +32,10 @@ import rx.Observable
interface StateMachineManager {
/**
* Starts the state machine manager, loading and starting the state machines in storage.
*
* @return `Future` which completes when SMM is fully started
*/
fun start(tokenizableServices: List<Any>)
fun start(tokenizableServices: List<Any>) : CordaFuture<Unit>
/**
* Stops the state machine manager gracefully, waiting until all but [allowedUnsuspendedFiberCount] flows reach the
@ -127,7 +130,7 @@ interface ExternalEvent {
val context: InvocationContext
/**
* A callback for the state machine to pass back the [Future] associated with the flow start to the submitter.
* A callback for the state machine to pass back the [CordaFuture] associated with the flow start to the submitter.
*/
fun wireUpFuture(flowFuture: CordaFuture<FlowStateMachine<T>>)

View File

@ -1,6 +1,7 @@
package net.corda.node.internal
import com.nhaarman.mockito_kotlin.doReturn
import com.nhaarman.mockito_kotlin.mock
import com.nhaarman.mockito_kotlin.whenever
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.delete
@ -189,8 +190,8 @@ class NodeTest {
rpcSettings = NodeRpcSettings(address = fakeAddress, adminAddress = null, ssl = null),
messagingServerAddress = null,
notary = null,
flowOverrides = FlowOverrideConfig(listOf())
flowOverrides = FlowOverrideConfig(listOf()),
configurationWithOptions = mock()
)
}
}

View File

@ -1,5 +1,6 @@
package net.corda.node.services.config
import com.nhaarman.mockito_kotlin.mock
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
import com.typesafe.config.ConfigParseOptions
@ -313,7 +314,8 @@ class NodeConfigurationImplTest {
rpcSettings = rpcSettings,
crlCheckSoftFail = true,
tlsCertCrlDistPoint = null,
flowOverrides = FlowOverrideConfig(listOf())
flowOverrides = FlowOverrideConfig(listOf()),
configurationWithOptions = mock()
)
}
}

View File

@ -16,9 +16,12 @@ import net.corda.core.internal.div
import net.corda.core.internal.inputStream
import net.corda.core.internal.readFully
import net.corda.core.node.ServiceHub
import net.corda.core.serialization.SerializeAsToken
import net.corda.core.serialization.SerializedBytes
import net.corda.core.serialization.internal.CheckpointSerializationDefaults
import net.corda.core.serialization.internal.checkpointSerialize
import net.corda.nodeapi.internal.lifecycle.NodeServicesContext
import net.corda.nodeapi.internal.lifecycle.NodeLifecycleEvent
import net.corda.node.internal.NodeStartup
import net.corda.node.services.persistence.DBCheckpointStorage
import net.corda.node.services.statemachine.Checkpoint
@ -39,7 +42,7 @@ import java.time.Clock
import java.time.Instant
import java.util.zip.ZipInputStream
class CheckpointDumperTest {
class CheckpointDumperImplTest {
@Rule
@JvmField
@ -50,12 +53,20 @@ class CheckpointDumperTest {
private val currentTimestamp = Instant.parse("2019-12-25T10:15:30.00Z")
private val baseDirectory = Files.createTempDirectory("CheckpointDumperTest")
private val file = baseDirectory / NodeStartup.LOGS_DIRECTORY_NAME /
"checkpoints_dump-${CheckpointDumper.TIME_FORMATTER.format(currentTimestamp)}.zip"
"checkpoints_dump-${CheckpointDumperImpl.TIME_FORMATTER.format(currentTimestamp)}.zip"
private lateinit var database: CordaPersistence
private lateinit var services: ServiceHub
private lateinit var checkpointStorage: DBCheckpointStorage
private val mockAfterStartEvent = {
val nodeServicesContextMock = mock<NodeServicesContext>()
whenever(nodeServicesContextMock.tokenizableServices).doReturn(emptyList<SerializeAsToken>())
val eventMock = mock<NodeLifecycleEvent.AfterNodeStart<*>>()
whenever(eventMock.nodeServicesContext).doReturn(nodeServicesContextMock)
eventMock
}()
@Before
fun setUp() {
val (db, mockServices) = MockServices.makeTestDatabaseAndPersistentServices(
@ -87,8 +98,8 @@ class CheckpointDumperTest {
@Test
fun testDumpCheckpoints() {
val dumper = CheckpointDumper(checkpointStorage, database, services, baseDirectory)
dumper.start(emptyList())
val dumper = CheckpointDumperImpl(checkpointStorage, database, services, baseDirectory)
dumper.update(mockAfterStartEvent)
// add a checkpoint
val (id, checkpoint) = newCheckpoint()
@ -96,7 +107,7 @@ class CheckpointDumperTest {
checkpointStorage.addCheckpoint(id, checkpoint)
}
dumper.dump()
dumper.dumpCheckpoints()
checkDumpFile()
}
@ -113,8 +124,8 @@ class CheckpointDumperTest {
// -javaagent:tools/checkpoint-agent/build/libs/checkpoint-agent.jar
@Test
fun testDumpCheckpointsAndAgentDiagnostics() {
val dumper = CheckpointDumper(checkpointStorage, database, services, Paths.get("."))
dumper.start(emptyList())
val dumper = CheckpointDumperImpl(checkpointStorage, database, services, Paths.get("."))
dumper.update(mockAfterStartEvent)
// add a checkpoint
val (id, checkpoint) = newCheckpoint()
@ -122,7 +133,7 @@ class CheckpointDumperTest {
checkpointStorage.addCheckpoint(id, checkpoint)
}
dumper.dump()
dumper.dumpCheckpoints()
// check existence of output zip file: checkpoints_dump-<date>.zip
// check existence of output agent log: checkpoints_agent-<data>.log
}

View File

@ -16,6 +16,7 @@ import net.corda.core.node.services.queryBy
import net.corda.core.node.services.vault.QueryCriteria.SoftLockingCondition
import net.corda.core.node.services.vault.QueryCriteria.SoftLockingType.LOCKED_ONLY
import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria
import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.transactions.LedgerTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.NonEmptySet
@ -90,7 +91,7 @@ class VaultSoftLockManagerTest {
cordappLoader: CordappLoader): VaultServiceInternal {
val node = this
val realVault = super.makeVaultService(keyManagementService, services, database, cordappLoader)
return object : VaultServiceInternal by realVault {
return object : SingletonSerializeAsToken(), VaultServiceInternal by realVault {
override fun softLockRelease(lockId: UUID, stateRefs: NonEmptySet<StateRef>?) {
// Should be called before flow is removed
assertEquals(1, node.started!!.smm.allStateMachines.size)

View File

@ -499,6 +499,10 @@ fun <T : SerializeAsToken> createMockCordaService(serviceHub: MockServices, serv
override val database: CordaTransactionSupport
get() = throw UnsupportedOperationException()
override fun register(priority: Int, observer: ServiceLifecycleObserver) {
throw UnsupportedOperationException()
}
}
return MockAppServiceHubImpl(serviceHub, serviceConstructor).serviceInstance
}

View File

@ -2,6 +2,7 @@ package net.corda.testing.node.internal
import com.nhaarman.mockito_kotlin.doReturn
import com.nhaarman.mockito_kotlin.whenever
import net.corda.common.configuration.parsing.internal.ConfigurationWithOptions
import net.corda.core.DoNotImplement
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SecureHash
@ -616,6 +617,7 @@ private fun mockNodeConfiguration(certificatesDirectory: Path): NodeConfiguratio
doReturn(5.seconds.toMillis()).whenever(it).additionalNodeInfoPollingFrequencyMsec
doReturn(null).whenever(it).devModeOptions
doReturn(NetworkParameterAcceptanceSettings()).whenever(it).networkParameterAcceptanceSettings
doReturn(rigorousMock<ConfigurationWithOptions>()).whenever(it).configurationWithOptions
}
}

View File

@ -5,6 +5,8 @@ import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.PartyAndCertificate
import net.corda.core.internal.PLATFORM_VERSION
import net.corda.core.internal.ThreadBox
import net.corda.core.internal.concurrent.OpenFuture
import net.corda.core.internal.concurrent.openFuture
import net.corda.core.messaging.MessageRecipients
import net.corda.core.node.services.PartyInfo
import net.corda.core.serialization.SingletonSerializeAsToken
@ -36,6 +38,8 @@ class MockNodeMessagingService(private val configuration: NodeConfiguration,
@Volatile
private var running = true
override val ready: OpenFuture<Void?> = openFuture()
private inner class InnerState {
val handlers: MutableList<Handler> = ArrayList()
val pendingRedelivery = LinkedHashSet<InMemoryMessagingNetwork.MessageTransfer>()
@ -85,6 +89,7 @@ class MockNodeMessagingService(private val configuration: NodeConfiguration,
}
network.addNotaryIdentity(this, notaryService)
ready.set(null)
}
override fun getAddressOfParty(partyInfo: PartyInfo): MessageRecipients {