ENT-5258 db schema set-up only via command line flag (#6280)

Removing the ability to initialise schema from the node config, and add a new sub-command to initialise the schema (that does not do anything else and exits afterwards).
Also adding a command line flag that allow app schema to be maintained by hibernate for legacy cordapps, tests or rapid development.
Patching up mock net and driver test frameworks so they create the required schemas for tests to work, defaulting schema migration and hibernate schema management to true to match pre-existing behaviour.
Modified network bootstrapper to run an initial schema set-up so it can register nodes.
This commit is contained in:
Christian Sailer
2020-05-22 16:27:10 +01:00
committed by GitHub
parent 8a0916b2a2
commit 70f1ea0a9d
33 changed files with 438 additions and 291 deletions

View File

@ -75,6 +75,13 @@ constructor(private val initSerEnv: Boolean,
"generate-node-info"
)
private val createSchemasCmd = listOf(
Paths.get(System.getProperty("java.home"), "bin", "java").toString(),
"-jar",
"corda.jar",
"run-migration-scripts"
)
private const val LOGS_DIR_NAME = "logs"
private val jarsThatArentCordapps = setOf("corda.jar", "runnodes.jar")
@ -92,7 +99,9 @@ constructor(private val initSerEnv: Boolean,
}
val executor = Executors.newFixedThreadPool(numParallelProcesses)
return try {
nodeDirs.map { executor.fork { generateNodeInfo(it) } }.transpose().getOrThrow()
nodeDirs.map { executor.fork {
createDbSchemas(it)
generateNodeInfo(it) } }.transpose().getOrThrow()
} finally {
warningTimer.cancel()
executor.shutdownNow()
@ -100,23 +109,31 @@ constructor(private val initSerEnv: Boolean,
}
private fun generateNodeInfo(nodeDir: Path): Path {
runNodeJob(nodeInfoGenCmd, nodeDir, "node-info-gen.log")
return nodeDir.list { paths ->
paths.filter { it.fileName.toString().startsWith(NODE_INFO_FILE_NAME_PREFIX) }.findFirst().get()
}
}
private fun createDbSchemas(nodeDir: Path) {
runNodeJob(createSchemasCmd, nodeDir, "node-run-migration.log")
}
private fun runNodeJob(command: List<String>, nodeDir: Path, logfileName: String) {
val logsDir = (nodeDir / LOGS_DIR_NAME).createDirectories()
val nodeInfoGenFile = (logsDir / "node-info-gen.log").toFile()
val process = ProcessBuilder(nodeInfoGenCmd)
val nodeRedirectFile = (logsDir / logfileName).toFile()
val process = ProcessBuilder(command)
.directory(nodeDir.toFile())
.redirectErrorStream(true)
.redirectOutput(nodeInfoGenFile)
.redirectOutput(nodeRedirectFile)
.apply { environment()["CAPSULE_CACHE_DIR"] = "../.cache" }
.start()
try {
if (!process.waitFor(3, TimeUnit.MINUTES)) {
process.destroyForcibly()
printNodeInfoGenLogToConsole(nodeInfoGenFile)
}
printNodeInfoGenLogToConsole(nodeInfoGenFile) { process.exitValue() == 0 }
return nodeDir.list { paths ->
paths.filter { it.fileName.toString().startsWith(NODE_INFO_FILE_NAME_PREFIX) }.findFirst().get()
printNodeOutputToConsoleAndThrow(nodeRedirectFile)
}
if (process.exitValue() != 0) printNodeOutputToConsoleAndThrow(nodeRedirectFile)
} catch (e: InterruptedException) {
// Don't leave this process dangling if the thread is interrupted.
process.destroyForcibly()
@ -124,18 +141,16 @@ constructor(private val initSerEnv: Boolean,
}
}
private fun printNodeInfoGenLogToConsole(nodeInfoGenFile: File, check: (() -> Boolean) = { true }) {
if (!check.invoke()) {
val nodeDir = nodeInfoGenFile.parent
val nodeIdentifier = try {
ConfigFactory.parseFile((nodeDir / "node.conf").toFile()).getString("myLegalName")
} catch (e: ConfigException) {
nodeDir
}
System.err.println("#### Error while generating node info file $nodeIdentifier ####")
nodeInfoGenFile.inputStream().copyTo(System.err)
throw IllegalStateException("Error while generating node info file. Please check the logs in $nodeDir.")
private fun printNodeOutputToConsoleAndThrow(stdoutFile: File) {
val nodeDir = stdoutFile.parent
val nodeIdentifier = try {
ConfigFactory.parseFile((nodeDir / "node.conf").toFile()).getString("myLegalName")
} catch (e: ConfigException) {
nodeDir
}
System.err.println("#### Error while generating node info file $nodeIdentifier ####")
stdoutFile.inputStream().copyTo(System.err)
throw IllegalStateException("Error while generating node info file. Please check the logs in $nodeDir.")
}
const val DEFAULT_MAX_MESSAGE_SIZE: Int = 10485760

View File

@ -31,24 +31,12 @@ import javax.sql.DataSource
*/
const val NODE_DATABASE_PREFIX = "node_"
enum class SchemaInitializationType{
NONE,
VALIDATE,
UPDATE
}
// This class forms part of the node config and so any changes to it must be handled with care
data class DatabaseConfig(
val initialiseSchema: Boolean = Defaults.initialiseSchema,
val initialiseAppSchema: SchemaInitializationType = Defaults.initialiseAppSchema,
val transactionIsolationLevel: TransactionIsolationLevel = Defaults.transactionIsolationLevel,
val exportHibernateJMXStatistics: Boolean = Defaults.exportHibernateJMXStatistics,
val mappedSchemaCacheSize: Long = Defaults.mappedSchemaCacheSize
) {
object Defaults {
val initialiseSchema = true
val initialiseAppSchema = SchemaInitializationType.UPDATE
val transactionIsolationLevel = TransactionIsolationLevel.REPEATABLE_READ
val exportHibernateJMXStatistics = false
val mappedSchemaCacheSize = 100L
}
@ -67,6 +55,10 @@ enum class TransactionIsolationLevel {
*/
val jdbcString = "TRANSACTION_$name"
val jdbcValue: Int = java.sql.Connection::class.java.getField(jdbcString).get(null) as Int
companion object{
val default = REPEATABLE_READ
}
}
internal val _prohibitDatabaseAccess = ThreadLocal.withInitial { false }
@ -103,20 +95,21 @@ class CordaPersistence(
attributeConverters: Collection<AttributeConverter<*, *>> = emptySet(),
customClassLoader: ClassLoader? = null,
val closeConnection: Boolean = true,
val errorHandler: DatabaseTransaction.(e: Exception) -> Unit = {}
val errorHandler: DatabaseTransaction.(e: Exception) -> Unit = {},
allowHibernateToManageAppSchema: Boolean = false
) : Closeable {
companion object {
private val log = contextLogger()
}
private val defaultIsolationLevel = databaseConfig.transactionIsolationLevel
private val defaultIsolationLevel = TransactionIsolationLevel.default
val hibernateConfig: HibernateConfiguration by lazy {
transaction {
try {
HibernateConfiguration(schemas, databaseConfig, attributeConverters, jdbcUrl, cacheFactory, customClassLoader)
HibernateConfiguration(schemas, databaseConfig, attributeConverters, jdbcUrl, cacheFactory, customClassLoader, allowHibernateToManageAppSchema)
} catch (e: Exception) {
when (e) {
is SchemaManagementException -> throw HibernateSchemaChangeException("Incompatible schema change detected. Please run the node with database.initialiseSchema=true. Reason: ${e.message}", e)
is SchemaManagementException -> throw HibernateSchemaChangeException("Incompatible schema change detected. Please run schema migration scripts (node with sub-command run-migration-scripts). Reason: ${e.message}", e)
else -> throw HibernateConfigException("Could not create Hibernate configuration: ${e.message}", e)
}
}

View File

@ -23,7 +23,8 @@ class HibernateConfiguration(
private val attributeConverters: Collection<AttributeConverter<*, *>>,
jdbcUrl: String,
cacheFactory: NamedCacheFactory,
val customClassLoader: ClassLoader? = null
val customClassLoader: ClassLoader? = null,
val allowHibernateToManageAppSchema: Boolean = false
) {
companion object {
private val logger = contextLogger()
@ -64,7 +65,7 @@ class HibernateConfiguration(
fun sessionFactoryForSchemas(key: Set<MappedSchema>): SessionFactory = sessionFactories.get(key, ::makeSessionFactoryForSchemas)!!
private fun makeSessionFactoryForSchemas(schemas: Set<MappedSchema>): SessionFactory {
val sessionFactory = sessionFactoryFactory.makeSessionFactoryForSchemas(databaseConfig, schemas, customClassLoader, attributeConverters)
val sessionFactory = sessionFactoryFactory.makeSessionFactoryForSchemas(databaseConfig, schemas, customClassLoader, attributeConverters, allowHibernateToManageAppSchema)
// export Hibernate JMX statistics
if (databaseConfig.exportHibernateJMXStatistics)

View File

@ -25,7 +25,6 @@ import kotlin.concurrent.withLock
class SchemaMigration(
val schemas: Set<MappedSchema>,
val dataSource: DataSource,
private val databaseConfig: DatabaseConfig,
cordappLoader: CordappLoader? = null,
private val currentDirectory: Path?,
// This parameter is used by the vault state migration to establish what the node's legal identity is when setting up
@ -50,29 +49,18 @@ class SchemaMigration(
private val classLoader = cordappLoader?.appClassLoader ?: Thread.currentThread().contextClassLoader
/**
* Main entry point to the schema migration.
* Called during node startup.
*/
fun nodeStartup(existingCheckpoints: Boolean) {
when {
databaseConfig.initialiseSchema -> {
migrateOlderDatabaseToUseLiquibase(existingCheckpoints)
runMigration(existingCheckpoints)
}
else -> checkState()
}
}
/**
* Will run the Liquibase migration on the actual database.
*/
private fun runMigration(existingCheckpoints: Boolean) = doRunMigration(run = true, check = false, existingCheckpoints = existingCheckpoints)
fun runMigration(existingCheckpoints: Boolean) {
migrateOlderDatabaseToUseLiquibase(existingCheckpoints)
doRunMigration(run = true, check = false, existingCheckpoints = existingCheckpoints)
}
/**
* Ensures that the database is up to date with the latest migration changes.
*/
private fun checkState() = doRunMigration(run = false, check = true)
fun checkState() = doRunMigration(run = false, check = true)
/** Create a resourse accessor that aggregates the changelogs included in the schemas into one dynamic stream. */
private class CustomResourceAccessor(val dynamicInclude: String, val changelogList: List<String?>, classLoader: ClassLoader) : ClassLoaderResourceAccessor(classLoader) {
@ -269,6 +257,6 @@ class CheckpointsException : DatabaseMigrationException("Attempting to update th
class DatabaseIncompatibleException(@Suppress("MemberVisibilityCanBePrivate") private val reason: String) : DatabaseMigrationException(errorMessageFor(reason)) {
internal companion object {
fun errorMessageFor(reason: String): String = "Incompatible database schema version detected, please run the node with configuration option database.initialiseSchema=true. Reason: $reason"
fun errorMessageFor(reason: String): String = "Incompatible database schema version detected, please run schema migration scripts (node with sub-command run-migration-scripts). Reason: $reason"
}
}

View File

@ -5,7 +5,7 @@ import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.toHexString
import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.nodeapi.internal.persistence.HibernateConfiguration
import net.corda.nodeapi.internal.persistence.SchemaInitializationType
import net.corda.nodeapi.internal.persistence.TransactionIsolationLevel
import org.hibernate.SessionFactory
import org.hibernate.boot.Metadata
import org.hibernate.boot.MetadataBuilder
@ -26,22 +26,19 @@ abstract class BaseSessionFactoryFactory : CordaSessionFactoryFactory {
private val logger = contextLogger()
}
open fun buildHibernateConfig(databaseConfig: DatabaseConfig, metadataSources: MetadataSources): Configuration {
open fun buildHibernateConfig(databaseConfig: DatabaseConfig, metadataSources: MetadataSources, allowHibernateToManageAppSchema: Boolean): Configuration {
val hbm2dll: String =
if (databaseConfig.initialiseSchema && databaseConfig.initialiseAppSchema == SchemaInitializationType.UPDATE) {
if (allowHibernateToManageAppSchema) {
"update"
} else if ((!databaseConfig.initialiseSchema && databaseConfig.initialiseAppSchema == SchemaInitializationType.UPDATE)
|| databaseConfig.initialiseAppSchema == SchemaInitializationType.VALIDATE) {
} else {
"validate"
} else {
"none"
}
// We set a connection provider as the auto schema generation requires it. The auto schema generation will not
// necessarily remain and would likely be replaced by something like Liquibase. For now it is very convenient though.
return Configuration(metadataSources).setProperty("hibernate.connection.provider_class", HibernateConfiguration.NodeDatabaseConnectionProvider::class.java.name)
.setProperty("hibernate.format_sql", "true")
.setProperty("javax.persistence.validation.mode", "none")
.setProperty("hibernate.connection.isolation", databaseConfig.transactionIsolationLevel.jdbcValue.toString())
.setProperty("hibernate.connection.isolation", TransactionIsolationLevel.default.jdbcValue.toString())
.setProperty("hibernate.hbm2ddl.auto", hbm2dll)
.setProperty("hibernate.jdbc.time_zone", "UTC")
}
@ -88,12 +85,13 @@ abstract class BaseSessionFactoryFactory : CordaSessionFactoryFactory {
databaseConfig: DatabaseConfig,
schemas: Set<MappedSchema>,
customClassLoader: ClassLoader?,
attributeConverters: Collection<AttributeConverter<*, *>>): SessionFactory {
attributeConverters: Collection<AttributeConverter<*, *>>,
allowHibernateToMananageAppSchema: Boolean): SessionFactory {
logger.info("Creating session factory for schemas: $schemas")
val serviceRegistry = BootstrapServiceRegistryBuilder().build()
val metadataSources = MetadataSources(serviceRegistry)
val config = buildHibernateConfig(databaseConfig, metadataSources)
val config = buildHibernateConfig(databaseConfig, metadataSources, allowHibernateToMananageAppSchema)
schemas.forEach { schema ->
schema.mappedTypes.forEach { config.addAnnotatedClass(it) }
}

View File

@ -14,7 +14,8 @@ interface CordaSessionFactoryFactory {
databaseConfig: DatabaseConfig,
schemas: Set<MappedSchema>,
customClassLoader: ClassLoader?,
attributeConverters: Collection<AttributeConverter<*, *>>): SessionFactory
attributeConverters: Collection<AttributeConverter<*, *>>,
allowHibernateToMananageAppSchema: Boolean): SessionFactory
fun getExtraConfiguration(key: String): Any?
fun buildHibernateMetadata(metadataBuilder: MetadataBuilder, attributeConverters: Collection<AttributeConverter<*, *>>): Metadata
}