mirror of
https://github.com/corda/corda.git
synced 2025-01-20 11:39:09 +00:00
Merge pull request #33 from corda/parkri-external-observations
Buffer observations until database commit.
This commit is contained in:
commit
70dcab6361
@ -13,6 +13,8 @@ import kotlinx.support.jdk7.use
|
|||||||
import net.corda.core.crypto.newSecureRandom
|
import net.corda.core.crypto.newSecureRandom
|
||||||
import org.slf4j.Logger
|
import org.slf4j.Logger
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
import rx.Observer
|
||||||
|
import rx.subjects.PublishSubject
|
||||||
import rx.subjects.UnicastSubject
|
import rx.subjects.UnicastSubject
|
||||||
import java.io.BufferedInputStream
|
import java.io.BufferedInputStream
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
@ -363,5 +365,15 @@ fun <T> Observable<T>.bufferUntilSubscribed(): Observable<T> {
|
|||||||
return subject.doOnUnsubscribe { subscription.unsubscribe() }
|
return subject.doOnUnsubscribe { subscription.unsubscribe() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy an [Observer] to multiple other [Observer]s.
|
||||||
|
*/
|
||||||
|
fun <T> Observer<T>.tee(vararg teeTo: Observer<T>): Observer<T> {
|
||||||
|
val subject = PublishSubject.create<T>()
|
||||||
|
subject.subscribe(this)
|
||||||
|
teeTo.forEach { subject.subscribe(it) }
|
||||||
|
return subject
|
||||||
|
}
|
||||||
|
|
||||||
/** Allows summing big decimals that are in iterable collections */
|
/** Allows summing big decimals that are in iterable collections */
|
||||||
fun Iterable<BigDecimal>.sum(): BigDecimal = fold(BigDecimal.ZERO) { a, b -> a + b }
|
fun Iterable<BigDecimal>.sum(): BigDecimal = fold(BigDecimal.ZERO) { a, b -> a + b }
|
||||||
|
@ -100,8 +100,19 @@ interface VaultService {
|
|||||||
val currentVault: Vault
|
val currentVault: Vault
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Prefer the use of [updates] unless you know why you want to use this instead.
|
||||||
|
*
|
||||||
* Get a synchronous Observable of updates. When observations are pushed to the Observer, the Vault will already incorporate
|
* Get a synchronous Observable of updates. When observations are pushed to the Observer, the Vault will already incorporate
|
||||||
* the update.
|
* the update, and the database transaction associated with the update will still be open and current. If for some
|
||||||
|
* reason the processing crosses outside of the database transaction (for example, the update is pushed outside the current
|
||||||
|
* JVM or across to another [Thread] which is executing in a different database transaction) then the Vault may
|
||||||
|
* not incorporate the update due to racing with committing the current database transaction.
|
||||||
|
*/
|
||||||
|
val rawUpdates: Observable<Vault.Update>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a synchronous Observable of updates. When observations are pushed to the Observer, the Vault will already incorporate
|
||||||
|
* the update, and the database transaction associated with the update will have been committed and closed.
|
||||||
*/
|
*/
|
||||||
val updates: Observable<Vault.Update>
|
val updates: Observable<Vault.Update>
|
||||||
|
|
||||||
|
@ -3,9 +3,7 @@ package net.corda.docs
|
|||||||
import com.google.common.util.concurrent.SettableFuture
|
import com.google.common.util.concurrent.SettableFuture
|
||||||
import net.corda.core.contracts.*
|
import net.corda.core.contracts.*
|
||||||
import net.corda.core.getOrThrow
|
import net.corda.core.getOrThrow
|
||||||
import net.corda.core.node.ServiceHub
|
|
||||||
import net.corda.core.node.services.ServiceInfo
|
import net.corda.core.node.services.ServiceInfo
|
||||||
import net.corda.core.node.services.linearHeadsOfType
|
|
||||||
import net.corda.core.serialization.OpaqueBytes
|
import net.corda.core.serialization.OpaqueBytes
|
||||||
import net.corda.core.utilities.DUMMY_NOTARY
|
import net.corda.core.utilities.DUMMY_NOTARY
|
||||||
import net.corda.core.utilities.DUMMY_NOTARY_KEY
|
import net.corda.core.utilities.DUMMY_NOTARY_KEY
|
||||||
@ -18,7 +16,6 @@ import net.corda.testing.node.MockNetwork
|
|||||||
import org.junit.After
|
import org.junit.After
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.util.*
|
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
class FxTransactionBuildTutorialTest {
|
class FxTransactionBuildTutorialTest {
|
||||||
@ -69,13 +66,13 @@ class FxTransactionBuildTutorialTest {
|
|||||||
printBalances()
|
printBalances()
|
||||||
|
|
||||||
// Setup some futures on the vaults to await the arrival of the exchanged funds at both nodes
|
// Setup some futures on the vaults to await the arrival of the exchanged funds at both nodes
|
||||||
val done2 = SettableFuture.create<Map<Currency, Amount<Currency>>>()
|
val done2 = SettableFuture.create<Unit>()
|
||||||
val done3 = SettableFuture.create<Map<Currency, Amount<Currency>>>()
|
val done3 = SettableFuture.create<Unit>()
|
||||||
val subs2 = nodeA.services.vaultService.updates.subscribe {
|
val subs2 = nodeA.services.vaultService.updates.subscribe {
|
||||||
done2.set(nodeA.services.vaultService.cashBalances)
|
done2.set(Unit)
|
||||||
}
|
}
|
||||||
val subs3 = nodeB.services.vaultService.updates.subscribe {
|
val subs3 = nodeB.services.vaultService.updates.subscribe {
|
||||||
done3.set(nodeB.services.vaultService.cashBalances)
|
done3.set(Unit)
|
||||||
}
|
}
|
||||||
// Now run the actual Fx exchange
|
// Now run the actual Fx exchange
|
||||||
val doIt = nodeA.services.startFlow(ForeignExchangeFlow("trade1",
|
val doIt = nodeA.services.startFlow(ForeignExchangeFlow("trade1",
|
||||||
@ -86,8 +83,14 @@ class FxTransactionBuildTutorialTest {
|
|||||||
// wait for the flow to finish and the vault updates to be done
|
// wait for the flow to finish and the vault updates to be done
|
||||||
doIt.resultFuture.getOrThrow()
|
doIt.resultFuture.getOrThrow()
|
||||||
// Get the balances when the vault updates
|
// Get the balances when the vault updates
|
||||||
val balancesA = done2.get()
|
done2.get()
|
||||||
val balancesB = done3.get()
|
val balancesA = databaseTransaction(nodeA.database) {
|
||||||
|
nodeA.services.vaultService.cashBalances
|
||||||
|
}
|
||||||
|
done3.get()
|
||||||
|
val balancesB = databaseTransaction(nodeB.database) {
|
||||||
|
nodeB.services.vaultService.cashBalances
|
||||||
|
}
|
||||||
subs2.unsubscribe()
|
subs2.unsubscribe()
|
||||||
subs3.unsubscribe()
|
subs3.unsubscribe()
|
||||||
println("BalanceA\n" + balancesA)
|
println("BalanceA\n" + balancesA)
|
||||||
|
@ -53,7 +53,7 @@ class WorkflowTransactionBuildTutorialTest {
|
|||||||
net.stopNodes()
|
net.stopNodes()
|
||||||
}
|
}
|
||||||
|
|
||||||
//@Test
|
@Test
|
||||||
fun `Run workflow to completion`() {
|
fun `Run workflow to completion`() {
|
||||||
// Setup a vault subscriber to wait for successful upload of the proposal to NodeB
|
// Setup a vault subscriber to wait for successful upload of the proposal to NodeB
|
||||||
val done1 = SettableFuture.create<Unit>()
|
val done1 = SettableFuture.create<Unit>()
|
||||||
|
@ -247,7 +247,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, val netwo
|
|||||||
|
|
||||||
// TODO: this model might change but for now it provides some de-coupling
|
// TODO: this model might change but for now it provides some de-coupling
|
||||||
// Add vault observers
|
// Add vault observers
|
||||||
CashBalanceAsMetricsObserver(services)
|
CashBalanceAsMetricsObserver(services, database)
|
||||||
ScheduledActivityObserver(services)
|
ScheduledActivityObserver(services)
|
||||||
HibernateObserver(services)
|
HibernateObserver(services)
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ import net.corda.node.services.api.ServiceHubInternal
|
|||||||
*/
|
*/
|
||||||
class ScheduledActivityObserver(val services: ServiceHubInternal) {
|
class ScheduledActivityObserver(val services: ServiceHubInternal) {
|
||||||
init {
|
init {
|
||||||
services.vaultService.updates.subscribe { update ->
|
services.vaultService.rawUpdates.subscribe { update ->
|
||||||
update.consumed.forEach { services.schedulerService.unscheduleStateActivity(it) }
|
update.consumed.forEach { services.schedulerService.unscheduleStateActivity(it) }
|
||||||
update.produced.forEach { scheduleStateActivity(it, services.flowLogicRefFactory) }
|
update.produced.forEach { scheduleStateActivity(it, services.flowLogicRefFactory) }
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,7 @@ import net.corda.node.services.network.NetworkMapService.Companion.SUBSCRIPTION_
|
|||||||
import net.corda.node.services.network.NetworkMapService.FetchMapResponse
|
import net.corda.node.services.network.NetworkMapService.FetchMapResponse
|
||||||
import net.corda.node.services.network.NetworkMapService.SubscribeResponse
|
import net.corda.node.services.network.NetworkMapService.SubscribeResponse
|
||||||
import net.corda.node.utilities.AddOrRemove
|
import net.corda.node.utilities.AddOrRemove
|
||||||
|
import net.corda.node.utilities.bufferUntilDatabaseCommit
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.subjects.PublishSubject
|
import rx.subjects.PublishSubject
|
||||||
import java.security.SignatureException
|
import java.security.SignatureException
|
||||||
@ -42,7 +43,9 @@ open class InMemoryNetworkMapCache : SingletonSerializeAsToken(), NetworkMapCach
|
|||||||
override val partyNodes: List<NodeInfo> get() = registeredNodes.map { it.value }
|
override val partyNodes: List<NodeInfo> get() = registeredNodes.map { it.value }
|
||||||
override val networkMapNodes: List<NodeInfo> get() = getNodesWithService(NetworkMapService.type)
|
override val networkMapNodes: List<NodeInfo> get() = getNodesWithService(NetworkMapService.type)
|
||||||
private val _changed = PublishSubject.create<MapChange>()
|
private val _changed = PublishSubject.create<MapChange>()
|
||||||
override val changed: Observable<MapChange> = _changed
|
override val changed: Observable<MapChange> get() = _changed
|
||||||
|
private val changePublisher: rx.Observer<MapChange> get() = _changed.bufferUntilDatabaseCommit()
|
||||||
|
|
||||||
private val _registrationFuture = SettableFuture.create<Unit>()
|
private val _registrationFuture = SettableFuture.create<Unit>()
|
||||||
override val mapServiceRegistered: ListenableFuture<Unit> get() = _registrationFuture
|
override val mapServiceRegistered: ListenableFuture<Unit> get() = _registrationFuture
|
||||||
|
|
||||||
@ -91,9 +94,9 @@ open class InMemoryNetworkMapCache : SingletonSerializeAsToken(), NetworkMapCach
|
|||||||
synchronized(_changed) {
|
synchronized(_changed) {
|
||||||
val previousNode = registeredNodes.put(node.legalIdentity, node)
|
val previousNode = registeredNodes.put(node.legalIdentity, node)
|
||||||
if (previousNode == null) {
|
if (previousNode == null) {
|
||||||
_changed.onNext(MapChange.Added(node))
|
changePublisher.onNext(MapChange.Added(node))
|
||||||
} else if (previousNode != node) {
|
} else if (previousNode != node) {
|
||||||
_changed.onNext(MapChange.Modified(node, previousNode))
|
changePublisher.onNext(MapChange.Modified(node, previousNode))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -101,7 +104,7 @@ open class InMemoryNetworkMapCache : SingletonSerializeAsToken(), NetworkMapCach
|
|||||||
override fun removeNode(node: NodeInfo) {
|
override fun removeNode(node: NodeInfo) {
|
||||||
synchronized(_changed) {
|
synchronized(_changed) {
|
||||||
registeredNodes.remove(node.legalIdentity)
|
registeredNodes.remove(node.legalIdentity)
|
||||||
_changed.onNext(MapChange.Removed(node))
|
changePublisher.onNext(MapChange.Removed(node))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,7 +50,7 @@ class DBTransactionMappingStorage : StateMachineRecordedTransactionMappingStorag
|
|||||||
override fun addMapping(stateMachineRunId: StateMachineRunId, transactionId: SecureHash) {
|
override fun addMapping(stateMachineRunId: StateMachineRunId, transactionId: SecureHash) {
|
||||||
mutex.locked {
|
mutex.locked {
|
||||||
stateMachineTransactionMap[transactionId] = stateMachineRunId
|
stateMachineTransactionMap[transactionId] = stateMachineRunId
|
||||||
updates.onNext(StateMachineTransactionMapping(stateMachineRunId, transactionId))
|
updates.bufferUntilDatabaseCommit().onNext(StateMachineTransactionMapping(stateMachineRunId, transactionId))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,7 +40,7 @@ class DBTransactionStorage : TransactionStorage {
|
|||||||
val old = txStorage.get(transaction.id)
|
val old = txStorage.get(transaction.id)
|
||||||
if (old == null) {
|
if (old == null) {
|
||||||
txStorage.put(transaction.id, transaction)
|
txStorage.put(transaction.id, transaction)
|
||||||
updatesPublisher.onNext(transaction)
|
updatesPublisher.bufferUntilDatabaseCommit().onNext(transaction)
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
|
@ -35,7 +35,7 @@ class HibernateObserver(services: ServiceHubInternal) {
|
|||||||
val sessionFactories = ConcurrentHashMap<MappedSchema, SessionFactory>()
|
val sessionFactories = ConcurrentHashMap<MappedSchema, SessionFactory>()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
services.vaultService.updates.subscribe { persist(it.produced) }
|
services.vaultService.rawUpdates.subscribe { persist(it.produced) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sessionFactoryForSchema(schema: MappedSchema): SessionFactory {
|
private fun sessionFactoryForSchema(schema: MappedSchema): SessionFactory {
|
||||||
|
@ -29,6 +29,7 @@ import net.corda.node.services.api.CheckpointStorage
|
|||||||
import net.corda.node.services.api.ServiceHubInternal
|
import net.corda.node.services.api.ServiceHubInternal
|
||||||
import net.corda.node.utilities.AddOrRemove
|
import net.corda.node.utilities.AddOrRemove
|
||||||
import net.corda.node.utilities.AffinityExecutor
|
import net.corda.node.utilities.AffinityExecutor
|
||||||
|
import net.corda.node.utilities.bufferUntilDatabaseCommit
|
||||||
import net.corda.node.utilities.isolatedTransaction
|
import net.corda.node.utilities.isolatedTransaction
|
||||||
import org.apache.activemq.artemis.utils.ReusableLatch
|
import org.apache.activemq.artemis.utils.ReusableLatch
|
||||||
import org.jetbrains.exposed.sql.Database
|
import org.jetbrains.exposed.sql.Database
|
||||||
@ -93,7 +94,7 @@ class StateMachineManager(val serviceHub: ServiceHubInternal,
|
|||||||
val changesPublisher = PublishSubject.create<Change>()
|
val changesPublisher = PublishSubject.create<Change>()
|
||||||
|
|
||||||
fun notifyChangeObservers(psm: FlowStateMachineImpl<*>, addOrRemove: AddOrRemove) {
|
fun notifyChangeObservers(psm: FlowStateMachineImpl<*>, addOrRemove: AddOrRemove) {
|
||||||
changesPublisher.onNext(Change(psm.logic, addOrRemove, psm.id))
|
changesPublisher.bufferUntilDatabaseCommit().onNext(Change(psm.logic, addOrRemove, psm.id))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -393,13 +394,14 @@ class StateMachineManager(val serviceHub: ServiceHubInternal,
|
|||||||
* restarted with checkpointed state machines in the storage service.
|
* restarted with checkpointed state machines in the storage service.
|
||||||
*/
|
*/
|
||||||
fun <T> add(logic: FlowLogic<T>): FlowStateMachine<T> {
|
fun <T> add(logic: FlowLogic<T>): FlowStateMachine<T> {
|
||||||
val fiber = createFiber(logic)
|
|
||||||
// We swap out the parent transaction context as using this frequently leads to a deadlock as we wait
|
// We swap out the parent transaction context as using this frequently leads to a deadlock as we wait
|
||||||
// on the flow completion future inside that context. The problem is that any progress checkpoints are
|
// on the flow completion future inside that context. The problem is that any progress checkpoints are
|
||||||
// unable to acquire the table lock and move forward till the calling transaction finishes.
|
// unable to acquire the table lock and move forward till the calling transaction finishes.
|
||||||
// Committing in line here on a fresh context ensure we can progress.
|
// Committing in line here on a fresh context ensure we can progress.
|
||||||
isolatedTransaction(database) {
|
val fiber = isolatedTransaction(database) {
|
||||||
|
val fiber = createFiber(logic)
|
||||||
updateCheckpoint(fiber)
|
updateCheckpoint(fiber)
|
||||||
|
fiber
|
||||||
}
|
}
|
||||||
// If we are not started then our checkpoint will be picked up during start
|
// If we are not started then our checkpoint will be picked up during start
|
||||||
mutex.locked {
|
mutex.locked {
|
||||||
|
@ -3,12 +3,14 @@ package net.corda.node.services.vault
|
|||||||
import com.codahale.metrics.Gauge
|
import com.codahale.metrics.Gauge
|
||||||
import net.corda.core.node.services.VaultService
|
import net.corda.core.node.services.VaultService
|
||||||
import net.corda.node.services.api.ServiceHubInternal
|
import net.corda.node.services.api.ServiceHubInternal
|
||||||
|
import net.corda.node.utilities.databaseTransaction
|
||||||
|
import org.jetbrains.exposed.sql.Database
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class observes the vault and reflect current cash balances as exposed metrics in the monitoring service.
|
* This class observes the vault and reflect current cash balances as exposed metrics in the monitoring service.
|
||||||
*/
|
*/
|
||||||
class CashBalanceAsMetricsObserver(val serviceHubInternal: ServiceHubInternal) {
|
class CashBalanceAsMetricsObserver(val serviceHubInternal: ServiceHubInternal, val database: Database) {
|
||||||
init {
|
init {
|
||||||
// TODO: Need to consider failure scenarios. This needs to run if the TX is successfully recorded
|
// TODO: Need to consider failure scenarios. This needs to run if the TX is successfully recorded
|
||||||
serviceHubInternal.vaultService.updates.subscribe { update ->
|
serviceHubInternal.vaultService.updates.subscribe { update ->
|
||||||
@ -29,13 +31,15 @@ class CashBalanceAsMetricsObserver(val serviceHubInternal: ServiceHubInternal) {
|
|||||||
//
|
//
|
||||||
// Note: exported as pennies.
|
// Note: exported as pennies.
|
||||||
val m = serviceHubInternal.monitoringService.metrics
|
val m = serviceHubInternal.monitoringService.metrics
|
||||||
for ((key, value) in vault.cashBalances) {
|
databaseTransaction(database) {
|
||||||
val metric = balanceMetrics.getOrPut(key) {
|
for ((key, value) in vault.cashBalances) {
|
||||||
val newMetric = BalanceMetric()
|
val metric = balanceMetrics.getOrPut(key) {
|
||||||
m.register("VaultBalances.${key}Pennies", newMetric)
|
val newMetric = BalanceMetric()
|
||||||
newMetric
|
m.register("VaultBalances.${key}Pennies", newMetric)
|
||||||
|
newMetric
|
||||||
|
}
|
||||||
|
metric.pennies = value.quantity
|
||||||
}
|
}
|
||||||
metric.pennies = value.quantity
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ import net.corda.core.node.ServiceHub
|
|||||||
import net.corda.core.node.services.Vault
|
import net.corda.core.node.services.Vault
|
||||||
import net.corda.core.node.services.VaultService
|
import net.corda.core.node.services.VaultService
|
||||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||||
|
import net.corda.core.tee
|
||||||
import net.corda.core.transactions.TransactionBuilder
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
import net.corda.core.transactions.WireTransaction
|
import net.corda.core.transactions.WireTransaction
|
||||||
import net.corda.core.utilities.loggerFor
|
import net.corda.core.utilities.loggerFor
|
||||||
@ -80,6 +81,9 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT
|
|||||||
}
|
}
|
||||||
|
|
||||||
val _updatesPublisher = PublishSubject.create<Vault.Update>()
|
val _updatesPublisher = PublishSubject.create<Vault.Update>()
|
||||||
|
val _rawUpdatesPublisher = PublishSubject.create<Vault.Update>()
|
||||||
|
// For use during publishing only.
|
||||||
|
val updatesPublisher: rx.Observer<Vault.Update> get() = _updatesPublisher.bufferUntilDatabaseCommit().tee(_rawUpdatesPublisher)
|
||||||
|
|
||||||
fun allUnconsumedStates(): Iterable<StateAndRef<ContractState>> {
|
fun allUnconsumedStates(): Iterable<StateAndRef<ContractState>> {
|
||||||
// Order by txhash for if and when transaction storage has some caching.
|
// Order by txhash for if and when transaction storage has some caching.
|
||||||
@ -104,6 +108,9 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT
|
|||||||
|
|
||||||
override val currentVault: Vault get() = mutex.locked { Vault(allUnconsumedStates()) }
|
override val currentVault: Vault get() = mutex.locked { Vault(allUnconsumedStates()) }
|
||||||
|
|
||||||
|
override val rawUpdates: Observable<Vault.Update>
|
||||||
|
get() = mutex.locked { _rawUpdatesPublisher }
|
||||||
|
|
||||||
override val updates: Observable<Vault.Update>
|
override val updates: Observable<Vault.Update>
|
||||||
get() = mutex.locked { _updatesPublisher }
|
get() = mutex.locked { _updatesPublisher }
|
||||||
|
|
||||||
@ -127,7 +134,7 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT
|
|||||||
if (netDelta != Vault.NoUpdate) {
|
if (netDelta != Vault.NoUpdate) {
|
||||||
mutex.locked {
|
mutex.locked {
|
||||||
recordUpdate(netDelta)
|
recordUpdate(netDelta)
|
||||||
_updatesPublisher.onNext(netDelta)
|
updatesPublisher.onNext(netDelta)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return currentVault
|
return currentVault
|
||||||
|
@ -7,9 +7,13 @@ import net.corda.core.crypto.CompositeKey
|
|||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.crypto.parsePublicKeyBase58
|
import net.corda.core.crypto.parsePublicKeyBase58
|
||||||
import net.corda.core.crypto.toBase58String
|
import net.corda.core.crypto.toBase58String
|
||||||
|
import net.corda.node.utilities.StrandLocalTransactionManager.Boundary
|
||||||
import org.jetbrains.exposed.sql.*
|
import org.jetbrains.exposed.sql.*
|
||||||
import org.jetbrains.exposed.sql.transactions.TransactionInterface
|
import org.jetbrains.exposed.sql.transactions.TransactionInterface
|
||||||
import org.jetbrains.exposed.sql.transactions.TransactionManager
|
import org.jetbrains.exposed.sql.transactions.TransactionManager
|
||||||
|
import rx.Observable
|
||||||
|
import rx.subjects.PublishSubject
|
||||||
|
import rx.subjects.UnicastSubject
|
||||||
import java.io.Closeable
|
import java.io.Closeable
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import java.sql.Connection
|
import java.sql.Connection
|
||||||
@ -18,6 +22,7 @@ import java.time.LocalDate
|
|||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.ZoneOffset
|
import java.time.ZoneOffset
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Table prefix for all tables owned by the node module.
|
* Table prefix for all tables owned by the node module.
|
||||||
@ -66,12 +71,19 @@ fun <T> isolatedTransaction(database: Database, block: Transaction.() -> T): T {
|
|||||||
* over each other. So here we use a companion object to hold them as [ThreadLocal] and [StrandLocalTransactionManager]
|
* over each other. So here we use a companion object to hold them as [ThreadLocal] and [StrandLocalTransactionManager]
|
||||||
* is otherwise effectively stateless so it's replacement does not matter. The [ThreadLocal] is then set correctly and
|
* is otherwise effectively stateless so it's replacement does not matter. The [ThreadLocal] is then set correctly and
|
||||||
* explicitly just prior to initiating a transaction in [databaseTransaction] and [createDatabaseTransaction] above.
|
* explicitly just prior to initiating a transaction in [databaseTransaction] and [createDatabaseTransaction] above.
|
||||||
|
*
|
||||||
|
* The [StrandLocalTransactionManager] instances have an [Observable] of the transaction close [Boundary]s which
|
||||||
|
* facilitates the use of [Observable.afterDatabaseCommit] to create event streams that only emit once the database
|
||||||
|
* transaction is closed and the data has been persisted and becomes visible to other observers.
|
||||||
*/
|
*/
|
||||||
class StrandLocalTransactionManager(initWithDatabase: Database) : TransactionManager {
|
class StrandLocalTransactionManager(initWithDatabase: Database) : TransactionManager {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
private val TX_ID = Key<UUID>()
|
||||||
|
|
||||||
private val threadLocalDb = ThreadLocal<Database>()
|
private val threadLocalDb = ThreadLocal<Database>()
|
||||||
private val threadLocalTx = ThreadLocal<Transaction>()
|
private val threadLocalTx = ThreadLocal<Transaction>()
|
||||||
|
private val databaseToInstance = ConcurrentHashMap<Database, StrandLocalTransactionManager>()
|
||||||
|
|
||||||
fun setThreadLocalTx(tx: Transaction?): Pair<Database?, Transaction?> {
|
fun setThreadLocalTx(tx: Transaction?): Pair<Database?, Transaction?> {
|
||||||
val oldTx = threadLocalTx.get()
|
val oldTx = threadLocalTx.get()
|
||||||
@ -89,10 +101,21 @@ class StrandLocalTransactionManager(initWithDatabase: Database) : TransactionMan
|
|||||||
set(value: Database) {
|
set(value: Database) {
|
||||||
threadLocalDb.set(value)
|
threadLocalDb.set(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val transactionId: UUID
|
||||||
|
get() = threadLocalTx.get()?.getUserData(TX_ID) ?: throw IllegalStateException("Was expecting to find transaction set on current strand: ${Strand.currentStrand()}")
|
||||||
|
|
||||||
|
val manager: StrandLocalTransactionManager get() = databaseToInstance[database]!!
|
||||||
|
|
||||||
|
val transactionBoundaries: PublishSubject<Boundary> get() = manager._transactionBoundaries
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
data class Boundary(val txId: UUID)
|
||||||
|
|
||||||
|
private val _transactionBoundaries = PublishSubject.create<Boundary>()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
database = initWithDatabase
|
|
||||||
// Found a unit test that was forgetting to close the database transactions. When you close() on the top level
|
// Found a unit test that was forgetting to close the database transactions. When you close() on the top level
|
||||||
// database transaction it will reset the threadLocalTx back to null, so if it isn't then there is still a
|
// database transaction it will reset the threadLocalTx back to null, so if it isn't then there is still a
|
||||||
// databae transaction open. The [databaseTransaction] helper above handles this in a finally clause for you
|
// databae transaction open. The [databaseTransaction] helper above handles this in a finally clause for you
|
||||||
@ -100,16 +123,23 @@ class StrandLocalTransactionManager(initWithDatabase: Database) : TransactionMan
|
|||||||
if (threadLocalTx.get() != null) {
|
if (threadLocalTx.get() != null) {
|
||||||
throw IllegalStateException("Was not expecting to find existing database transaction on current strand when setting database: ${Strand.currentStrand()}, ${threadLocalTx.get()}")
|
throw IllegalStateException("Was not expecting to find existing database transaction on current strand when setting database: ${Strand.currentStrand()}, ${threadLocalTx.get()}")
|
||||||
}
|
}
|
||||||
|
database = initWithDatabase
|
||||||
|
databaseToInstance[database] = this
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun newTransaction(isolation: Int): Transaction = Transaction(StrandLocalTransaction(database, isolation, threadLocalTx)).apply {
|
override fun newTransaction(isolation: Int): Transaction {
|
||||||
threadLocalTx.set(this)
|
val impl = StrandLocalTransaction(database, isolation, threadLocalTx, transactionBoundaries)
|
||||||
|
return Transaction(impl).apply {
|
||||||
|
threadLocalTx.set(this)
|
||||||
|
putUserData(TX_ID, impl.id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun currentOrNull(): Transaction? = threadLocalTx.get()
|
override fun currentOrNull(): Transaction? = threadLocalTx.get()
|
||||||
|
|
||||||
// Direct copy of [ThreadLocalTransaction].
|
// Direct copy of [ThreadLocalTransaction].
|
||||||
private class StrandLocalTransaction(override val db: Database, isolation: Int, val threadLocal: ThreadLocal<Transaction>) : TransactionInterface {
|
private class StrandLocalTransaction(override val db: Database, isolation: Int, val threadLocal: ThreadLocal<Transaction>, val transactionBoundaries: PublishSubject<Boundary>) : TransactionInterface {
|
||||||
|
val id = UUID.randomUUID()
|
||||||
|
|
||||||
override val connection: Connection by lazy(LazyThreadSafetyMode.NONE) {
|
override val connection: Connection by lazy(LazyThreadSafetyMode.NONE) {
|
||||||
db.connector().apply {
|
db.connector().apply {
|
||||||
@ -133,13 +163,33 @@ class StrandLocalTransactionManager(initWithDatabase: Database) : TransactionMan
|
|||||||
override fun close() {
|
override fun close() {
|
||||||
connection.close()
|
connection.close()
|
||||||
threadLocal.set(outerTransaction)
|
threadLocal.set(outerTransaction)
|
||||||
|
if (outerTransaction == null) {
|
||||||
|
transactionBoundaries.onNext(Boundary(id))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Buffer observations until after the current database transaction has been closed. Observations are never
|
||||||
|
* dropped, simply delayed.
|
||||||
|
*
|
||||||
|
* Primarily for use by component authors to publish observations during database transactions without racing against
|
||||||
|
* closing the database transaction.
|
||||||
|
*
|
||||||
|
* For examples, see the call hierarchy of this function.
|
||||||
|
*/
|
||||||
|
fun <T : Any> rx.Observer<T>.bufferUntilDatabaseCommit(): rx.Observer<T> {
|
||||||
|
val currentTxId = StrandLocalTransactionManager.transactionId
|
||||||
|
val databaseTxBoundary: Observable<StrandLocalTransactionManager.Boundary> = StrandLocalTransactionManager.transactionBoundaries.filter { it.txId == currentTxId }.first()
|
||||||
|
val subject = UnicastSubject.create<T>()
|
||||||
|
subject.delaySubscription(databaseTxBoundary).subscribe(this)
|
||||||
|
databaseTxBoundary.doOnCompleted { subject.onCompleted() }
|
||||||
|
return subject
|
||||||
|
}
|
||||||
|
|
||||||
// Composite columns for use with below Exposed helpers.
|
// Composite columns for use with below Exposed helpers.
|
||||||
data class PartyColumns(val name: Column<String>, val owningKey: Column<CompositeKey>)
|
data class PartyColumns(val name: Column<String>, val owningKey: Column<CompositeKey>)
|
||||||
|
|
||||||
data class StateRefColumns(val txId: Column<SecureHash>, val index: Column<Int>)
|
data class StateRefColumns(val txId: Column<SecureHash>, val index: Column<Int>)
|
||||||
data class TxnNoteColumns(val txId: Column<SecureHash>, val note: Column<String>)
|
data class TxnNoteColumns(val txId: Column<SecureHash>, val note: Column<String>)
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ import com.typesafe.config.ConfigFactory
|
|||||||
import net.corda.core.crypto.composite
|
import net.corda.core.crypto.composite
|
||||||
import net.corda.core.crypto.generateKeyPair
|
import net.corda.core.crypto.generateKeyPair
|
||||||
import net.corda.core.messaging.Message
|
import net.corda.core.messaging.Message
|
||||||
|
import net.corda.core.messaging.RPCOps
|
||||||
import net.corda.core.messaging.createMessage
|
import net.corda.core.messaging.createMessage
|
||||||
import net.corda.core.node.services.DEFAULT_SESSION_ID
|
import net.corda.core.node.services.DEFAULT_SESSION_ID
|
||||||
import net.corda.core.utilities.LogHelper
|
import net.corda.core.utilities.LogHelper
|
||||||
@ -16,7 +17,6 @@ import net.corda.node.services.config.NodeConfiguration
|
|||||||
import net.corda.node.services.config.configureWithDevSSLCertificate
|
import net.corda.node.services.config.configureWithDevSSLCertificate
|
||||||
import net.corda.node.services.messaging.ArtemisMessagingServer
|
import net.corda.node.services.messaging.ArtemisMessagingServer
|
||||||
import net.corda.node.services.messaging.NodeMessagingClient
|
import net.corda.node.services.messaging.NodeMessagingClient
|
||||||
import net.corda.core.messaging.RPCOps
|
|
||||||
import net.corda.node.services.network.InMemoryNetworkMapCache
|
import net.corda.node.services.network.InMemoryNetworkMapCache
|
||||||
import net.corda.node.services.network.NetworkMapService
|
import net.corda.node.services.network.NetworkMapService
|
||||||
import net.corda.node.services.transactions.PersistentUniquenessProvider
|
import net.corda.node.services.transactions.PersistentUniquenessProvider
|
||||||
|
@ -5,6 +5,7 @@ import net.corda.core.crypto.generateKeyPair
|
|||||||
import net.corda.core.getOrThrow
|
import net.corda.core.getOrThrow
|
||||||
import net.corda.core.node.services.ServiceInfo
|
import net.corda.core.node.services.ServiceInfo
|
||||||
import net.corda.node.services.network.NetworkMapService
|
import net.corda.node.services.network.NetworkMapService
|
||||||
|
import net.corda.node.utilities.databaseTransaction
|
||||||
import net.corda.testing.expect
|
import net.corda.testing.expect
|
||||||
import net.corda.testing.node.MockNetwork
|
import net.corda.testing.node.MockNetwork
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
@ -30,7 +31,9 @@ class InMemoryNetworkMapCacheTest {
|
|||||||
// Node A currently knows only about itself, so this returns node A
|
// Node A currently knows only about itself, so this returns node A
|
||||||
assertEquals(nodeA.netMapCache.getNodeByCompositeKey(keyPair.public.composite), nodeA.info)
|
assertEquals(nodeA.netMapCache.getNodeByCompositeKey(keyPair.public.composite), nodeA.info)
|
||||||
|
|
||||||
nodeA.netMapCache.addNode(nodeB.info)
|
databaseTransaction(nodeA.database) {
|
||||||
|
nodeA.netMapCache.addNode(nodeB.info)
|
||||||
|
}
|
||||||
// Now both nodes match, so it throws an error
|
// Now both nodes match, so it throws an error
|
||||||
expect<IllegalStateException> {
|
expect<IllegalStateException> {
|
||||||
nodeA.netMapCache.getNodeByCompositeKey(keyPair.public.composite)
|
nodeA.netMapCache.getNodeByCompositeKey(keyPair.public.composite)
|
||||||
|
@ -16,6 +16,7 @@ import net.corda.node.services.network.NetworkMapService.Companion.REGISTER_FLOW
|
|||||||
import net.corda.node.services.network.NetworkMapService.Companion.SUBSCRIPTION_FLOW_TOPIC
|
import net.corda.node.services.network.NetworkMapService.Companion.SUBSCRIPTION_FLOW_TOPIC
|
||||||
import net.corda.node.services.network.NodeRegistration
|
import net.corda.node.services.network.NodeRegistration
|
||||||
import net.corda.node.utilities.AddOrRemove
|
import net.corda.node.utilities.AddOrRemove
|
||||||
|
import net.corda.node.utilities.databaseTransaction
|
||||||
import net.corda.testing.node.MockNetwork
|
import net.corda.testing.node.MockNetwork
|
||||||
import net.corda.testing.node.MockNetwork.MockNode
|
import net.corda.testing.node.MockNetwork.MockNode
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
@ -201,7 +202,9 @@ class InMemoryNetworkMapServiceTest : AbstractNetworkMapServiceTest() {
|
|||||||
fun success() {
|
fun success() {
|
||||||
val (mapServiceNode, registerNode) = network.createTwoNodes()
|
val (mapServiceNode, registerNode) = network.createTwoNodes()
|
||||||
val service = mapServiceNode.inNodeNetworkMapService!! as InMemoryNetworkMapService
|
val service = mapServiceNode.inNodeNetworkMapService!! as InMemoryNetworkMapService
|
||||||
success(mapServiceNode, registerNode, { service }, { })
|
databaseTransaction(mapServiceNode.database) {
|
||||||
|
success(mapServiceNode, registerNode, { service }, { })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -0,0 +1,142 @@
|
|||||||
|
package net.corda.node.utilities
|
||||||
|
|
||||||
|
import com.google.common.util.concurrent.SettableFuture
|
||||||
|
import net.corda.core.tee
|
||||||
|
import net.corda.testing.node.makeTestDataSourceProperties
|
||||||
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
|
import org.jetbrains.exposed.sql.transactions.TransactionManager
|
||||||
|
import org.junit.Test
|
||||||
|
import rx.Observable
|
||||||
|
import rx.subjects.PublishSubject
|
||||||
|
|
||||||
|
class ObservablesTests {
|
||||||
|
|
||||||
|
private fun isInDatabaseTransaction(): Boolean = (TransactionManager.currentOrNull() != null)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `bufferUntilDatabaseCommit delays until transaction closed`() {
|
||||||
|
val (toBeClosed, database) = configureDatabase(makeTestDataSourceProperties())
|
||||||
|
|
||||||
|
val subject = PublishSubject.create<Int>()
|
||||||
|
val observable: Observable<Int> = subject
|
||||||
|
|
||||||
|
val firstEvent = SettableFuture.create<Pair<Int, Boolean>>()
|
||||||
|
val secondEvent = SettableFuture.create<Pair<Int, Boolean>>()
|
||||||
|
|
||||||
|
observable.first().subscribe { firstEvent.set(it to isInDatabaseTransaction()) }
|
||||||
|
observable.skip(1).first().subscribe { secondEvent.set(it to isInDatabaseTransaction()) }
|
||||||
|
|
||||||
|
databaseTransaction(database) {
|
||||||
|
val delayedSubject = subject.bufferUntilDatabaseCommit()
|
||||||
|
assertThat(subject).isNotEqualTo(delayedSubject)
|
||||||
|
delayedSubject.onNext(0)
|
||||||
|
subject.onNext(1)
|
||||||
|
assertThat(firstEvent.isDone).isTrue()
|
||||||
|
assertThat(secondEvent.isDone).isFalse()
|
||||||
|
}
|
||||||
|
assertThat(secondEvent.isDone).isTrue()
|
||||||
|
|
||||||
|
assertThat(firstEvent.get()).isEqualTo(1 to true)
|
||||||
|
assertThat(secondEvent.get()).isEqualTo(0 to false)
|
||||||
|
|
||||||
|
toBeClosed.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `bufferUntilDatabaseCommit delays until transaction closed repeatable`() {
|
||||||
|
val (toBeClosed, database) = configureDatabase(makeTestDataSourceProperties())
|
||||||
|
|
||||||
|
val subject = PublishSubject.create<Int>()
|
||||||
|
val observable: Observable<Int> = subject
|
||||||
|
|
||||||
|
val firstEvent = SettableFuture.create<Pair<Int, Boolean>>()
|
||||||
|
val secondEvent = SettableFuture.create<Pair<Int, Boolean>>()
|
||||||
|
|
||||||
|
observable.first().subscribe { firstEvent.set(it to isInDatabaseTransaction()) }
|
||||||
|
observable.skip(1).first().subscribe { secondEvent.set(it to isInDatabaseTransaction()) }
|
||||||
|
|
||||||
|
databaseTransaction(database) {
|
||||||
|
val delayedSubject = subject.bufferUntilDatabaseCommit()
|
||||||
|
assertThat(subject).isNotEqualTo(delayedSubject)
|
||||||
|
delayedSubject.onNext(0)
|
||||||
|
assertThat(firstEvent.isDone).isFalse()
|
||||||
|
assertThat(secondEvent.isDone).isFalse()
|
||||||
|
}
|
||||||
|
assertThat(firstEvent.isDone).isTrue()
|
||||||
|
assertThat(firstEvent.get()).isEqualTo(0 to false)
|
||||||
|
assertThat(secondEvent.isDone).isFalse()
|
||||||
|
|
||||||
|
databaseTransaction(database) {
|
||||||
|
val delayedSubject = subject.bufferUntilDatabaseCommit()
|
||||||
|
assertThat(subject).isNotEqualTo(delayedSubject)
|
||||||
|
delayedSubject.onNext(1)
|
||||||
|
assertThat(secondEvent.isDone).isFalse()
|
||||||
|
}
|
||||||
|
assertThat(secondEvent.isDone).isTrue()
|
||||||
|
assertThat(secondEvent.get()).isEqualTo(1 to false)
|
||||||
|
|
||||||
|
toBeClosed.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `tee correctly copies observations to multiple observers`() {
|
||||||
|
|
||||||
|
val subject1 = PublishSubject.create<Int>()
|
||||||
|
val subject2 = PublishSubject.create<Int>()
|
||||||
|
val subject3 = PublishSubject.create<Int>()
|
||||||
|
|
||||||
|
val event1 = SettableFuture.create<Int>()
|
||||||
|
val event2 = SettableFuture.create<Int>()
|
||||||
|
val event3 = SettableFuture.create<Int>()
|
||||||
|
|
||||||
|
subject1.subscribe { event1.set(it) }
|
||||||
|
subject2.subscribe { event2.set(it) }
|
||||||
|
subject3.subscribe { event3.set(it) }
|
||||||
|
|
||||||
|
val tee = subject1.tee(subject2, subject3)
|
||||||
|
tee.onNext(0)
|
||||||
|
|
||||||
|
assertThat(event1.isDone).isTrue()
|
||||||
|
assertThat(event2.isDone).isTrue()
|
||||||
|
assertThat(event3.isDone).isTrue()
|
||||||
|
assertThat(event1.get()).isEqualTo(0)
|
||||||
|
assertThat(event2.get()).isEqualTo(0)
|
||||||
|
assertThat(event3.get()).isEqualTo(0)
|
||||||
|
|
||||||
|
tee.onCompleted()
|
||||||
|
assertThat(subject1.hasCompleted()).isTrue()
|
||||||
|
assertThat(subject2.hasCompleted()).isTrue()
|
||||||
|
assertThat(subject3.hasCompleted()).isTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `combine tee and bufferUntilDatabaseCommit`() {
|
||||||
|
val (toBeClosed, database) = configureDatabase(makeTestDataSourceProperties())
|
||||||
|
|
||||||
|
val subject = PublishSubject.create<Int>()
|
||||||
|
val teed = PublishSubject.create<Int>()
|
||||||
|
|
||||||
|
val observable: Observable<Int> = subject
|
||||||
|
|
||||||
|
val firstEvent = SettableFuture.create<Pair<Int, Boolean>>()
|
||||||
|
val teedEvent = SettableFuture.create<Pair<Int, Boolean>>()
|
||||||
|
|
||||||
|
observable.first().subscribe { firstEvent.set(it to isInDatabaseTransaction()) }
|
||||||
|
|
||||||
|
teed.first().subscribe { teedEvent.set(it to isInDatabaseTransaction()) }
|
||||||
|
|
||||||
|
databaseTransaction(database) {
|
||||||
|
val delayedSubject = subject.bufferUntilDatabaseCommit().tee(teed)
|
||||||
|
assertThat(subject).isNotEqualTo(delayedSubject)
|
||||||
|
delayedSubject.onNext(0)
|
||||||
|
assertThat(firstEvent.isDone).isFalse()
|
||||||
|
assertThat(teedEvent.isDone).isTrue()
|
||||||
|
}
|
||||||
|
assertThat(firstEvent.isDone).isTrue()
|
||||||
|
|
||||||
|
assertThat(firstEvent.get()).isEqualTo(0 to false)
|
||||||
|
assertThat(teedEvent.get()).isEqualTo(0 to true)
|
||||||
|
|
||||||
|
toBeClosed.close()
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user