mirror of
https://github.com/corda/corda.git
synced 2025-06-13 20:58:19 +00:00
ENT-2848 Add caching to contract attachment versions (#4410)
* Refactor into attachment service Fix up mock service First caching version, but with no invalidation currently Set cache size Fix up after rebase Cache invalidation Formatting tidy up Sort out some nullability Add kdocs. Unit tests More unit tests Fix TODO Unit test fixes Unit test fixes Fixed concurrent invalidating transaction support. * Correct some transaction concurrency bug, including unit test. * Added some unit tests for the method I added to persistence. * Remove some blank lines * Review feedback * Fix imports
This commit is contained in:
@ -10,8 +10,10 @@ import rx.subjects.UnicastSubject
|
||||
import java.io.Closeable
|
||||
import java.sql.Connection
|
||||
import java.sql.SQLException
|
||||
import java.util.*
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import javax.persistence.AttributeConverter
|
||||
import javax.sql.DataSource
|
||||
|
||||
@ -110,9 +112,33 @@ class CordaPersistence(
|
||||
return contextTransactionOrNull ?: newTransaction(isolation)
|
||||
}
|
||||
|
||||
private val liveTransactions = ConcurrentHashMap<UUID, DatabaseTransaction>()
|
||||
|
||||
fun newTransaction(isolation: TransactionIsolationLevel = defaultIsolationLevel): DatabaseTransaction {
|
||||
val outerTransaction = contextTransactionOrNull
|
||||
return DatabaseTransaction(isolation.jdbcValue, contextTransactionOrNull, this).also {
|
||||
contextTransactionOrNull = it
|
||||
// Outer transaction only exists in a controlled scenario we can ignore.
|
||||
if (outerTransaction == null) {
|
||||
liveTransactions.put(it.id, it)
|
||||
it.onClose { liveTransactions.remove(it.id) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onAllOpenTransactionsClosed(callback: () -> Unit) {
|
||||
val allOpen = liveTransactions.values.toList()
|
||||
if (allOpen.isEmpty()) {
|
||||
callback()
|
||||
} else {
|
||||
val counter = AtomicInteger(allOpen.size)
|
||||
allOpen.forEach {
|
||||
it.onClose {
|
||||
if (counter.decrementAndGet() == 0) {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6,7 +6,7 @@ import org.hibernate.Session
|
||||
import org.hibernate.Transaction
|
||||
import rx.subjects.PublishSubject
|
||||
import java.sql.Connection
|
||||
import java.util.*
|
||||
import java.util.UUID
|
||||
import javax.persistence.EntityManager
|
||||
|
||||
fun currentDBSession(): Session = contextTransaction.session
|
||||
@ -71,6 +71,7 @@ class DatabaseTransaction(
|
||||
|
||||
internal val boundary = PublishSubject.create<CordaPersistence.Boundary>()
|
||||
private var committed = false
|
||||
private var closed = false
|
||||
|
||||
fun commit() {
|
||||
if (sessionDelegate.isInitialized()) {
|
||||
@ -96,7 +97,10 @@ class DatabaseTransaction(
|
||||
connection.close()
|
||||
contextTransactionOrNull = outerTransaction
|
||||
if (outerTransaction == null) {
|
||||
boundary.onNext(CordaPersistence.Boundary(id, committed))
|
||||
synchronized(this) {
|
||||
closed = true
|
||||
boundary.onNext(CordaPersistence.Boundary(id, committed))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -107,5 +111,10 @@ class DatabaseTransaction(
|
||||
fun onRollback(callback: () -> Unit) {
|
||||
boundary.filter { !it.success }.subscribe { callback() }
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun onClose(callback: () -> Unit) {
|
||||
if (closed) callback() else boundary.subscribe { callback() }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -8,16 +8,10 @@ import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.DEPLOYED_CORDAPP_UPLOADER
|
||||
import net.corda.core.internal.RPC_UPLOADER
|
||||
import net.corda.core.internal.cordapp.CordappImpl.Companion.DEFAULT_CORDAPP_VERSION
|
||||
import net.corda.core.node.ServicesForResolution
|
||||
import net.corda.core.node.services.AttachmentStorage
|
||||
import net.corda.core.node.services.NetworkParametersStorage
|
||||
import net.corda.core.node.services.vault.AttachmentQueryCriteria
|
||||
import net.corda.core.node.services.vault.AttachmentSort
|
||||
import net.corda.core.node.services.vault.Builder
|
||||
import net.corda.core.node.services.vault.Sort
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
@ -96,12 +90,8 @@ class AttachmentsClassLoaderStaticContractTests {
|
||||
doReturn("app").whenever(attachment).uploader
|
||||
doReturn(emptyList<Party>()).whenever(attachment).signerKeys
|
||||
val contractAttachmentId = SecureHash.randomSHA256()
|
||||
val attachmentQueryCriteria = AttachmentQueryCriteria.AttachmentsQueryCriteria(
|
||||
contractClassNamesCondition = Builder.equal(listOf(ATTACHMENT_PROGRAM_ID)),
|
||||
versionCondition = Builder.greaterThanOrEqual(DEFAULT_CORDAPP_VERSION),
|
||||
uploaderCondition = Builder.`in`(listOf(DEPLOYED_CORDAPP_UPLOADER, RPC_UPLOADER)))
|
||||
val attachmentSort = AttachmentSort(listOf(AttachmentSort.AttachmentSortColumn(AttachmentSort.AttachmentSortAttribute.VERSION, Sort.Direction.DESC)))
|
||||
doReturn(listOf(contractAttachmentId)).whenever(attachmentStorage).queryAttachments(attachmentQueryCriteria, attachmentSort)
|
||||
doReturn(contractAttachmentId).whenever(attachmentStorage)
|
||||
.getContractAttachmentWithHighestContractVersion(AttachmentDummyContract.ATTACHMENT_PROGRAM_ID, DEFAULT_CORDAPP_VERSION)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -0,0 +1,99 @@
|
||||
package net.corda.nodeapi.internal
|
||||
|
||||
import net.corda.node.services.schema.NodeSchemaService
|
||||
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
||||
import net.corda.testing.internal.configureDatabase
|
||||
import net.corda.testing.node.MockServices
|
||||
import org.junit.After
|
||||
import org.junit.Test
|
||||
import java.util.concurrent.Phaser
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import kotlin.concurrent.thread
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class CordaPersistenceTest {
|
||||
private val database = configureDatabase(MockServices.makeTestDataSourceProperties(),
|
||||
DatabaseConfig(),
|
||||
{ null }, { null },
|
||||
NodeSchemaService(emptySet()))
|
||||
|
||||
@After
|
||||
fun closeDatabase() {
|
||||
database.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `onAllOpenTransactionsClosed with zero transactions calls back immediately`() {
|
||||
val counter = AtomicInteger(0)
|
||||
database.onAllOpenTransactionsClosed { counter.incrementAndGet() }
|
||||
assertEquals(1, counter.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `onAllOpenTransactionsClosed with one transaction calls back after closing`() {
|
||||
val counter = AtomicInteger(0)
|
||||
database.transaction {
|
||||
database.onAllOpenTransactionsClosed { counter.incrementAndGet() }
|
||||
assertEquals(0, counter.get())
|
||||
}
|
||||
assertEquals(1, counter.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `onAllOpenTransactionsClosed after one transaction has closed calls back immediately`() {
|
||||
val counter = AtomicInteger(0)
|
||||
database.transaction {
|
||||
database.onAllOpenTransactionsClosed { counter.incrementAndGet() }
|
||||
assertEquals(0, counter.get())
|
||||
}
|
||||
assertEquals(1, counter.get())
|
||||
database.onAllOpenTransactionsClosed { counter.incrementAndGet() }
|
||||
assertEquals(2, counter.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `onAllOpenTransactionsClosed with two transactions calls back after closing both`() {
|
||||
val counter = AtomicInteger(0)
|
||||
val phaser = openTransactionInOtherThreadAndCloseWhenISay()
|
||||
// Wait for tx to be started.
|
||||
phaser.arriveAndAwaitAdvance()
|
||||
database.transaction {
|
||||
database.onAllOpenTransactionsClosed { counter.incrementAndGet() }
|
||||
assertEquals(0, counter.get())
|
||||
}
|
||||
assertEquals(0, counter.get())
|
||||
phaser.arriveAndAwaitAdvance()
|
||||
phaser.arriveAndAwaitAdvance()
|
||||
assertEquals(1, counter.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `onAllOpenTransactionsClosed with two transactions calls back after closing both - instigator closes last`() {
|
||||
val counter = AtomicInteger(0)
|
||||
val phaser = openTransactionInOtherThreadAndCloseWhenISay()
|
||||
// Wait for tx to be started.
|
||||
phaser.arriveAndAwaitAdvance()
|
||||
database.transaction {
|
||||
database.onAllOpenTransactionsClosed { counter.incrementAndGet() }
|
||||
assertEquals(0, counter.get())
|
||||
phaser.arriveAndAwaitAdvance()
|
||||
phaser.arriveAndAwaitAdvance()
|
||||
assertEquals(0, counter.get())
|
||||
}
|
||||
assertEquals(1, counter.get())
|
||||
}
|
||||
|
||||
private fun openTransactionInOtherThreadAndCloseWhenISay(): Phaser {
|
||||
val phaser = Phaser()
|
||||
phaser.bulkRegister(2)
|
||||
thread {
|
||||
database.transaction {
|
||||
phaser.arriveAndAwaitAdvance()
|
||||
phaser.arriveAndAwaitAdvance()
|
||||
}
|
||||
// Tell caller we have committed.
|
||||
phaser.arriveAndAwaitAdvance()
|
||||
}
|
||||
return phaser
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user