[CORDA-3243] - Improve CorDapp loading logic for duplicates (#5551)

This commit is contained in:
Dimos Raptis 2019-10-07 16:20:55 +01:00 committed by Shams Asari
parent 5ce0a540c8
commit ec4d20d987
4 changed files with 65 additions and 12 deletions

View File

@ -14,7 +14,7 @@ import org.assertj.core.api.Assertions.assertThat
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
class FlowsExecutionModeTests : NodeBasedTest(listOf("net.corda.finance.contracts", CashSchemaV1::class.packageName)) { class FlowsExecutionModeTests : NodeBasedTest(emptyList()) {
private val rpcUser = User("user1", "test", permissions = setOf(Permissions.all())) private val rpcUser = User("user1", "test", permissions = setOf(Permissions.all()))
private lateinit var node: NodeWithInfo private lateinit var node: NodeWithInfo

View File

@ -15,6 +15,8 @@ object CordappResolver {
private val logger = loggerFor<CordappResolver>() private val logger = loggerFor<CordappResolver>()
private val cordappClasses: ConcurrentHashMap<String, Set<Cordapp>> = ConcurrentHashMap() private val cordappClasses: ConcurrentHashMap<String, Set<Cordapp>> = ConcurrentHashMap()
private val insideInMemoryTest: Boolean by lazy { insideInMemoryTest() }
// TODO Use the StackWalker API once we migrate to Java 9+ // TODO Use the StackWalker API once we migrate to Java 9+
private var cordappResolver: () -> Cordapp? = { private var cordappResolver: () -> Cordapp? = {
Exception().stackTrace Exception().stackTrace
@ -25,9 +27,12 @@ object CordappResolver {
?.single() ?.single()
} }
/* /**
* Associates class names with CorDapps or logs a warning when a CorDapp is already registered for a given class. * Associates class names with CorDapps or logs a warning when a CorDapp is already registered for a given class.
* This could happen when trying to run different versions of the same CorDapp on the same node. * This could happen when trying to run different versions of the same CorDapp on the same node.
*
* @throws IllegalStateException when multiple CorDapps are registered for the same contract class,
* since this can lead to undefined behaviour.
*/ */
@Synchronized @Synchronized
fun register(cordapp: Cordapp) { fun register(cordapp: Cordapp) {
@ -39,12 +44,30 @@ object CordappResolver {
notAlreadyRegisteredClasses.forEach { cordappClasses[it] = setOf(cordapp) } notAlreadyRegisteredClasses.forEach { cordappClasses[it] = setOf(cordapp) }
for ((className, registeredCordapps) in alreadyRegistered) { for ((registeredClassName, registeredCordapps) in alreadyRegistered) {
if (registeredCordapps.any { it.jarHash == cordapp.jarHash }) continue val duplicateCordapps = registeredCordapps.filter { it.jarHash == cordapp.jarHash }.toSet()
if (className in contractClasses) {
logger.error("ATTENTION: More than one CorDapp installed on the node for contract $className. Please remove the previous version when upgrading to a new version.") if (duplicateCordapps.isNotEmpty()) {
logger.warnOnce("The CorDapp (name: ${cordapp.info.shortName}, file: ${cordapp.name}) " +
"is installed multiple times on the node. The following files correspond to the exact same content: " +
"${duplicateCordapps.map { it.name }}")
continue
} }
cordappClasses[className] = registeredCordapps + cordapp // During in-memory tests, the spawned nodes share the same CordappResolver, so detected conflicts can be spurious.
if (registeredClassName in contractClasses && !insideInMemoryTest) {
throw IllegalStateException("More than one CorDapp installed on the node for contract $registeredClassName. " +
"Please remove the previous version when upgrading to a new version.")
}
cordappClasses[registeredClassName] = registeredCordapps + cordapp
}
}
private fun insideInMemoryTest(): Boolean {
return Exception().stackTrace.any {
it.className.startsWith("net.corda.testing.node.internal.InternalMockNetwork") ||
it.className.startsWith("net.corda.testing.node.internal.InProcessNode") ||
it.className.startsWith("net.corda.testing.node.MockServices")
} }
} }

View File

@ -2,9 +2,11 @@ package net.corda.core.internal.cordapp
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
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.lang.IllegalStateException
import kotlin.test.assertEquals import kotlin.test.assertEquals
class CordappResolverTest { class CordappResolverTest {
@ -49,19 +51,42 @@ class CordappResolverTest {
} }
@Test @Test
fun `when different cordapps are registered for the same class, the resolver returns null`() { fun `when different cordapps are registered for the same (non-contract) class, the resolver returns null`() {
CordappResolver.register(CordappImpl.TEST_INSTANCE.copy( CordappResolver.register(CordappImpl.TEST_INSTANCE.copy(
contractClassNames = listOf(javaClass.name), contractClassNames = listOf("ContractClass1"),
minimumPlatformVersion = 3, minimumPlatformVersion = 3,
targetPlatformVersion = 222, targetPlatformVersion = 222,
jarHash = SecureHash.randomSHA256() jarHash = SecureHash.randomSHA256()
)) ))
CordappResolver.register(CordappImpl.TEST_INSTANCE.copy( CordappResolver.register(CordappImpl.TEST_INSTANCE.copy(
contractClassNames = listOf(javaClass.name), contractClassNames = listOf("ContractClass2"),
minimumPlatformVersion = 2, minimumPlatformVersion = 2,
targetPlatformVersion = 456, targetPlatformVersion = 456,
jarHash = SecureHash.randomSHA256() jarHash = SecureHash.randomSHA256()
)) ))
assertThat(CordappResolver.currentCordapp).isNull() assertThat(CordappResolver.currentCordapp).isNull()
} }
@Test
fun `when different cordapps are registered for the same (contract) class, the resolver throws an exception`() {
val firstCordapp = CordappImpl.TEST_INSTANCE.copy(
contractClassNames = listOf(javaClass.name),
minimumPlatformVersion = 3,
targetPlatformVersion = 222,
jarHash = SecureHash.randomSHA256()
)
val secondCordapp = CordappImpl.TEST_INSTANCE.copy(
contractClassNames = listOf(javaClass.name),
minimumPlatformVersion = 2,
targetPlatformVersion = 456,
jarHash = SecureHash.randomSHA256()
)
CordappResolver.register(firstCordapp)
assertThatThrownBy { CordappResolver.register(secondCordapp) }
.isInstanceOf(IllegalStateException::class.java)
.hasMessageContaining("More than one CorDapp installed on the node for contract ${javaClass.name}. " +
"Please remove the previous version when upgrading to a new version.")
}
} }

View File

@ -43,7 +43,8 @@ class AddressBindingFailureTests {
driver(DriverParameters(startNodesInProcess = false, driver(DriverParameters(startNodesInProcess = false,
notarySpecs = listOf(NotarySpec(notaryName)), notarySpecs = listOf(NotarySpec(notaryName)),
notaryCustomOverrides = mapOf("p2pAddress" to address.toString()), notaryCustomOverrides = mapOf("p2pAddress" to address.toString()),
portAllocation = portAllocation) portAllocation = portAllocation,
cordappsForAllNodes = emptyList())
) {} }.isInstanceOfSatisfying(IllegalStateException::class.java) { error -> ) {} }.isInstanceOfSatisfying(IllegalStateException::class.java) { error ->
assertThat(error.message).contains("Unable to start notaries") assertThat(error.message).contains("Unable to start notaries")
@ -56,7 +57,11 @@ class AddressBindingFailureTests {
ServerSocket(0).use { socket -> ServerSocket(0).use { socket ->
val address = InetSocketAddress("localhost", socket.localPort).toNetworkHostAndPort() val address = InetSocketAddress("localhost", socket.localPort).toNetworkHostAndPort()
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), inMemoryDB = false, portAllocation = portAllocation)) { driver(DriverParameters(startNodesInProcess = true,
notarySpecs = emptyList(),
inMemoryDB = false,
portAllocation = portAllocation,
cordappsForAllNodes = emptyList())) {
assertThatThrownBy { startNode(customOverrides = overrides(address)).getOrThrow() }.isInstanceOfSatisfying(AddressBindingException::class.java) { exception -> assertThatThrownBy { startNode(customOverrides = overrides(address)).getOrThrow() }.isInstanceOfSatisfying(AddressBindingException::class.java) { exception ->
assertThat(exception.addresses).contains(address).withFailMessage("Expected addresses to contain $address but was ${exception.addresses}.") assertThat(exception.addresses).contains(address).withFailMessage("Expected addresses to contain $address but was ${exception.addresses}.")