From a4037b374dc7c1c9c6fbb06e57249184fdd4ae9f Mon Sep 17 00:00:00 2001 From: Rick Parker Date: Wed, 19 Dec 2018 17:53:48 +0000 Subject: [PATCH] ENT-2848 More contract attachment caching to avoid database queries slowing the node down (#4433) --- .../core/node/services/AttachmentStorage.kt | 11 ++- .../core/transactions/TransactionBuilder.kt | 3 +- .../persistence/NodeAttachmentService.kt | 90 +++++++++++++------ .../testing/services/MockAttachmentStorage.kt | 6 +- 4 files changed, 79 insertions(+), 31 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/node/services/AttachmentStorage.kt b/core/src/main/kotlin/net/corda/core/node/services/AttachmentStorage.kt index ffc42a094b..5bb5b21395 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/AttachmentStorage.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/AttachmentStorage.kt @@ -78,12 +78,21 @@ interface AttachmentStorage { /** * Find the Attachment Id of the contract attachment with the highest version for a given contract class name - * from trusted upload sources. + * from trusted upload sources. If both a signed and unsigned attachment exist, prefer the signed one. * * @param contractClassName The fully qualified name of the contract class. * @param minContractVersion The minimum contract version that should be returned. * @return the [AttachmentId] of the contract, or null if none meet the criteria. */ fun getContractAttachmentWithHighestContractVersion(contractClassName: String, minContractVersion: Int): AttachmentId? + + /** + * Find the Attachment Ids of the contract attachments for a given contract class name + * from trusted upload sources. + * + * @param contractClassName The fully qualified name of the contract class. + * @return the [AttachmentId]s of the contract attachments, or an empty set if none meet the criteria. + */ + fun getContractAttachments(contractClassName: String): Set } diff --git a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt index 318b2fb4f4..e7c6c75fba 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt @@ -256,8 +256,7 @@ open class TransactionBuilder @JvmOverloads constructor( val outputHashConstraints = outputStates?.filter { it.constraint is HashAttachmentConstraint } ?: emptyList() val outputSignatureConstraints = outputStates?.filter { it.constraint is SignatureAttachmentConstraint } ?: emptyList() if (inputsHashConstraints.isNotEmpty() && (outputHashConstraints.isNotEmpty() || outputSignatureConstraints.isNotEmpty())) { - val attachmentQueryCriteria = AttachmentQueryCriteria.AttachmentsQueryCriteria(contractClassNamesCondition = Builder.equal(listOf(contractClassName))) - val attachmentIds = services.attachments.queryAttachments(attachmentQueryCriteria) + val attachmentIds = services.attachments.getContractAttachments(contractClassName) // only switchover if we have both signed and unsigned attachments for the given contract class name if (attachmentIds.isNotEmpty() && attachmentIds.size == 2) { val attachmentsToUse = attachmentIds.map { diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt b/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt index 80357c70f4..5f8c673dae 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt @@ -10,6 +10,7 @@ import net.corda.core.CordaRuntimeException import net.corda.core.contracts.Attachment import net.corda.core.contracts.ContractAttachment import net.corda.core.contracts.ContractClassName +import net.corda.core.contracts.Version import net.corda.core.crypto.SecureHash import net.corda.core.crypto.sha256 import net.corda.core.internal.* @@ -54,7 +55,6 @@ class NodeAttachmentService( cacheFactory: NamedCacheFactory, private val database: CordaPersistence ) : AttachmentStorageInternal, SingletonSerializeAsToken() { - // This is to break the circular dependency. lateinit var servicesForResolution: ServicesForResolution @@ -84,7 +84,7 @@ class NodeAttachmentService( } @Entity - @Table(name = "${NODE_DATABASE_PREFIX}attachments", indexes = [Index(name = "att_id_idx", columnList = "att_id")]) + @Table(name = "${NODE_DATABASE_PREFIX}attachments", indexes = [(Index(name = "att_id_idx", columnList = "att_id"))]) class DBAttachment( @Id @Column(name = "att_id", nullable = false) @@ -403,32 +403,68 @@ class NodeAttachmentService( } } - private val contractsCache = InfrequentlyMutatedCache>("NodeAttachmentService_contractAttachmentVersions", cacheFactory) + // Holds onto a signed and/or unsigned attachment (at least one or the other). + private data class AttachmentIds(val signed: AttachmentId?, val unsigned: AttachmentId?) { + init { + // One of them at least must exist. + check(signed != null || unsigned != null) + } + + fun toList(): List = + if(signed != null) { + if(unsigned != null) { + listOf(signed, unsigned) + } else listOf(signed) + } else listOf(unsigned!!) + } + + /** + * This caches contract attachment versions by contract class name. For each version, we support one signed and one unsigned attachment, since that is allowed. + * + * It is correctly invalidated as new attachments are uploaded. + */ + private val contractsCache = InfrequentlyMutatedCache>("NodeAttachmentService_contractAttachmentVersions", cacheFactory) + + private fun getContractAttachmentVersions(contractClassName: String): NavigableMap = contractsCache.get(contractClassName) { name -> + val attachmentQueryCriteria = AttachmentQueryCriteria.AttachmentsQueryCriteria(contractClassNamesCondition = Builder.equal(listOf(name)), + versionCondition = Builder.greaterThanOrEqual(0), uploaderCondition = Builder.`in`(TRUSTED_UPLOADERS)) + val attachmentSort = AttachmentSort(listOf(AttachmentSort.AttachmentSortColumn(AttachmentSort.AttachmentSortAttribute.VERSION, Sort.Direction.DESC))) + database.transaction { + val session = currentDBSession() + val criteriaBuilder = session.criteriaBuilder + + val criteriaQuery = criteriaBuilder.createQuery(DBAttachment::class.java) + val root = criteriaQuery.from(DBAttachment::class.java) + + val criteriaParser = HibernateAttachmentQueryCriteriaParser(criteriaBuilder, criteriaQuery, root) + + // parse criteria and build where predicates + criteriaParser.parse(attachmentQueryCriteria, attachmentSort) + + // prepare query for execution + val query = session.createQuery(criteriaQuery) + + // execution + TreeMap(query.resultList.groupBy { it.version }.map { makeAttachmentIds(it) }.toMap()) + } + } + + private fun makeAttachmentIds(it: Map.Entry>): Pair { + check(it.value.size <= 2) + val signed = it.value.filter { it.signers?.isNotEmpty() ?: false }.map { AttachmentId.parse(it.attId) }.singleOrNull() + val unsigned = it.value.filter { it.signers?.isEmpty() ?: true }.map { AttachmentId.parse(it.attId) }.singleOrNull() + return it.key to AttachmentIds(signed, unsigned) + } override fun getContractAttachmentWithHighestContractVersion(contractClassName: String, minContractVersion: Int): AttachmentId? { - val versions: NavigableMap = contractsCache.get(contractClassName) { name -> - val attachmentQueryCriteria = AttachmentQueryCriteria.AttachmentsQueryCriteria(contractClassNamesCondition = Builder.equal(listOf(name)), - versionCondition = Builder.greaterThanOrEqual(0), uploaderCondition = Builder.`in`(TRUSTED_UPLOADERS)) - val attachmentSort = AttachmentSort(listOf(AttachmentSort.AttachmentSortColumn(AttachmentSort.AttachmentSortAttribute.VERSION, Sort.Direction.DESC))) - database.transaction { - val session = currentDBSession() - val criteriaBuilder = session.criteriaBuilder - - val criteriaQuery = criteriaBuilder.createQuery(DBAttachment::class.java) - val root = criteriaQuery.from(DBAttachment::class.java) - - val criteriaParser = HibernateAttachmentQueryCriteriaParser(criteriaBuilder, criteriaQuery, root) - - // parse criteria and build where predicates - criteriaParser.parse(attachmentQueryCriteria, attachmentSort) - - // prepare query for execution - val query = session.createQuery(criteriaQuery) - - // execution - TreeMap(query.resultList.map { it.version to AttachmentId.parse(it.attId) }.toMap()) - } - } - return versions.tailMap(minContractVersion, true).lastEntry()?.value + val versions: NavigableMap = getContractAttachmentVersions(contractClassName) + val newestAttachmentIds = versions.tailMap(minContractVersion, true).lastEntry()?.value + return newestAttachmentIds?.toList()?.first() } + + override fun getContractAttachments(contractClassName: String): Set { + val versions: NavigableMap = getContractAttachmentVersions(contractClassName) + return versions.values.flatMap { it.toList() }.toSet() + } + } \ No newline at end of file diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/services/MockAttachmentStorage.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/services/MockAttachmentStorage.kt index d2aaa0c9cd..8dda8ff73b 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/services/MockAttachmentStorage.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/services/MockAttachmentStorage.kt @@ -29,7 +29,6 @@ import java.util.jar.JarInputStream * A mock implementation of [AttachmentStorage] for use within tests */ class MockAttachmentStorage : AttachmentStorage, SingletonSerializeAsToken() { - private data class ContractAttachmentMetadata(val name: ContractClassName, val version: Int, val isSigned: Boolean) private val _files = HashMap>() @@ -123,4 +122,9 @@ class MockAttachmentStorage : AttachmentStorage, SingletonSerializeAsToken() { val attachmentSort = AttachmentSort(listOf(AttachmentSort.AttachmentSortColumn(AttachmentSort.AttachmentSortAttribute.VERSION, Sort.Direction.DESC))) return queryAttachments(attachmentQueryCriteria, attachmentSort).firstOrNull() } + + override fun getContractAttachments(contractClassName: String): Set { + val attachmentQueryCriteria = AttachmentQueryCriteria.AttachmentsQueryCriteria(contractClassNamesCondition = Builder.equal(listOf(contractClassName))) + return queryAttachments(attachmentQueryCriteria).toSet() + } } \ No newline at end of file