diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 4137a25983..f17c8f5aac 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -66,6 +66,8 @@ Version 3.2 the same server. Current ``compatibilityZoneURL`` configurations remain valid. See both :doc:`corda-configuration-file` and :doc:`permissioning` for details. +* Table name with a typo changed from ``NODE_ATTCHMENTS_CONTRACTS`` to ``NODE_ATTACHMENTS_CONTRACTS``. + .. _changelog_v3.1: Version 3.1 diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/CordaPersistence.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/CordaPersistence.kt index f91a3fc24f..f9479b8442 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/CordaPersistence.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/CordaPersistence.kt @@ -82,6 +82,8 @@ class CordaPersistence( // Check not in read-only mode. transaction { check(!connection.metaData.isReadOnly) { "Database should not be readonly." } + + checkCorrectAttachmentsContractsTableName(connection) } } @@ -245,3 +247,28 @@ fun rx.Observable.wrapWithDatabaseTransaction(db: CordaPersistence? } } } + +/** Check if any nested cause is of [SQLException] type. */ +private fun Throwable.hasSQLExceptionCause(): Boolean = + when (cause) { + null -> false + is SQLException -> true + else -> cause?.hasSQLExceptionCause() ?: false + } + +class CouldNotCreateDataSourceException(override val message: String?, override val cause: Throwable? = null) : Exception() + +class IncompatibleAttachmentsContractsTableName(override val message: String?, override val cause: Throwable? = null) : Exception() + +private fun checkCorrectAttachmentsContractsTableName(connection: Connection) { + val correctName = "NODE_ATTACHMENTS_CONTRACTS" + val incorrectV30Name = "NODE_ATTACHMENTS_CONTRACT_CLASS_NAME" + val incorrectV31Name = "NODE_ATTCHMENTS_CONTRACTS" + + fun warning(incorrectName: String, version: String) = "The database contains the older table name $incorrectName instead of $correctName, see upgrade notes to migrate from Corda database version $version https://docs.corda.net/head/upgrade-notes.html." + + if (!connection.metaData.getTables(null, null, correctName, null).next()) { + if (connection.metaData.getTables(null, null, incorrectV30Name, null).next()) { throw IncompatibleAttachmentsContractsTableName(warning(incorrectV30Name, "3.0")) } + if (connection.metaData.getTables(null, null, incorrectV31Name, null).next()) { throw IncompatibleAttachmentsContractsTableName(warning(incorrectV31Name, "3.1")) } + } +} diff --git a/node/src/integration-test/kotlin/net/corda/node/persistence/FailNodeOnNotMigratedAttachmentContractsTableNameTests.kt b/node/src/integration-test/kotlin/net/corda/node/persistence/FailNodeOnNotMigratedAttachmentContractsTableNameTests.kt new file mode 100644 index 0000000000..4563cc13dc --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/node/persistence/FailNodeOnNotMigratedAttachmentContractsTableNameTests.kt @@ -0,0 +1,67 @@ +package net.corda.node.persistence + +import net.corda.client.rpc.CordaRPCClient +import net.corda.core.internal.packageName +import net.corda.core.messaging.startFlow +import net.corda.core.utilities.getOrThrow +import net.corda.node.services.Permissions +import net.corda.test.node.Message +import net.corda.test.node.MessageState +import net.corda.test.node.SendMessageFlow +import net.corda.testing.core.singleIdentity +import net.corda.testing.driver.DriverParameters +import net.corda.testing.driver.driver +import net.corda.testing.driver.internal.RandomFree +import net.corda.testing.node.User +import org.junit.Test +import java.nio.file.Path +import java.sql.DriverManager +import kotlin.test.* + +class FailNodeOnNotMigratedAttachmentContractsTableNameTests { + @Test + fun `node fails when detecting table name not migrated from version 3 dot 0`() { + `node fails when not detecting compatible table name`("NODE_ATTACHMENTS_CONTRACTS", "NODE_ATTACHMENTS_CONTRACT_CLASS_NAME") + } + + @Test + fun `node fails when detecting table name not migrated from version 3 dot 1`() { + `node fails when not detecting compatible table name`("NODE_ATTACHMENTS_CONTRACTS", "NODE_ATTCHMENTS_CONTRACTS") + } + + fun `node fails when not detecting compatible table name`(tableNameFromMapping: String, tableNameInDB: String) { + val user = User("mark", "dadada", setOf(Permissions.startFlow(), Permissions.invokeRpc("vaultQuery"))) + val message = Message("Hello world!") + val baseDir: Path = driver(DriverParameters(startNodesInProcess = true, + portAllocation = RandomFree, extraCordappPackagesToScan = listOf(MessageState::class.packageName))) { + val (nodeName, baseDir) = { + val nodeHandle = startNode(rpcUsers = listOf(user)).getOrThrow() + val nodeName = nodeHandle.nodeInfo.singleIdentity().name + CordaRPCClient(nodeHandle.rpcAddress).start(user.username, user.password).use { + it.proxy.startFlow(::SendMessageFlow, message, defaultNotaryIdentity).returnValue.getOrThrow() + } + nodeHandle.stop() + Pair(nodeName, nodeHandle.baseDirectory) + }() + + // replace the correct table name with one from the former release + DriverManager.getConnection("jdbc:h2:file://$baseDir/persistence", "sa", "").use { + it.createStatement().execute("ALTER TABLE $tableNameFromMapping RENAME TO $tableNameInDB") + it.commit() + } + assertFailsWith(net.corda.nodeapi.internal.persistence.IncompatibleAttachmentsContractsTableName::class) { + val nodeHandle = startNode(providedName = nodeName, rpcUsers = listOf(user)).getOrThrow() + nodeHandle.stop() + } + baseDir + } + + // check that the node didn't recreated the correct table matching it's entity mapping + val (hasTableFromMapping, hasTableFromDB) = DriverManager.getConnection("jdbc:h2:file://$baseDir/persistence", "sa", "").use { + Pair(it.metaData.getTables(null, null, tableNameFromMapping, null).next(), + it.metaData.getTables(null, null, tableNameInDB, null).next()) + } + assertFalse(hasTableFromMapping) + assertTrue(hasTableFromDB) + } +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/internal/Node.kt b/node/src/main/kotlin/net/corda/node/internal/Node.kt index 41076753a3..ad4658d3d9 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -2,6 +2,7 @@ package net.corda.node.internal import com.codahale.metrics.JmxReporter import net.corda.core.concurrent.CordaFuture +import net.corda.core.internal.Emoji import net.corda.core.internal.concurrent.openFuture import net.corda.core.internal.concurrent.thenMatch import net.corda.core.internal.div @@ -74,6 +75,13 @@ open class Node(configuration: NodeConfiguration, LoggerFactory.getLogger(loggerName).info(msg) } + fun printWarning(message: String) { + Emoji.renderIfSupported { + println("ATTENTION: $message") + } + staticLog.warn(message) + } + internal fun failStartUp(message: String): Nothing { println(message) println("Corda will now exit...") diff --git a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt index 15c9ac31da..2838fdc884 100644 --- a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt +++ b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt @@ -17,6 +17,7 @@ import net.corda.node.shell.InteractiveShell import net.corda.node.utilities.registration.HTTPNetworkRegistrationService import net.corda.node.utilities.registration.NetworkRegistrationHelper import net.corda.nodeapi.internal.addShutdownHook +import net.corda.nodeapi.internal.persistence.IncompatibleAttachmentsContractsTableName import org.fusesource.jansi.Ansi import org.fusesource.jansi.AnsiConsole import org.slf4j.bridge.SLF4JBridgeHandler @@ -113,6 +114,10 @@ open class NodeStartup(val args: Array) { try { cmdlineOptions.baseDirectory.createDirectories() startNode(conf, versionInfo, startTime, cmdlineOptions) + } catch (e: IncompatibleAttachmentsContractsTableName) { + e.message?.let { Node.printWarning(it) } + logger.error(e.message) + return false } catch (e: Exception) { if (e is Errors.NativeIoException && e.message?.contains("Address already in use") == true) { logger.error("One of the ports required by the Corda node is already in use.")