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:
Rick Parker
2018-12-17 15:14:14 +00:00
committed by GitHub
parent fe3182d22f
commit 20e5bbf56f
12 changed files with 625 additions and 42 deletions

View File

@ -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()
}
}
}
}
}

View File

@ -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() }
}
}

View File

@ -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

View File

@ -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
}
}