ENT-5134 Discoverable Hibernate Session Factory Factory (#6091)

* Introduce CordaSessionFactoryFactory interface and the H2 implememntation
* Load SessionFactoryFactory via service loader
* Add Postgres SessionFactoryFactory
* Add extraConfiguration function for SessionFactoryFactory implementations to expose special config values.
This commit is contained in:
Christian Sailer 2020-03-27 11:29:40 +00:00 committed by GitHub
parent 678fb6eb94
commit ccca605865
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 278 additions and 129 deletions

View File

@ -5,24 +5,15 @@ import net.corda.core.internal.NamedCacheFactory
import net.corda.core.internal.castIfPossible
import net.corda.core.schemas.MappedSchema
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.toHexString
import net.corda.nodeapi.internal.persistence.factory.CordaSessionFactoryFactory
import org.hibernate.SessionFactory
import org.hibernate.boot.Metadata
import org.hibernate.boot.MetadataBuilder
import org.hibernate.boot.MetadataSources
import org.hibernate.boot.registry.BootstrapServiceRegistryBuilder
import org.hibernate.boot.registry.classloading.internal.ClassLoaderServiceImpl
import org.hibernate.boot.registry.classloading.spi.ClassLoaderService
import org.hibernate.cfg.Configuration
import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider
import org.hibernate.service.UnknownUnwrapTypeException
import org.hibernate.type.AbstractSingleColumnStandardBasicType
import org.hibernate.type.MaterializedBlobType
import org.hibernate.type.descriptor.java.PrimitiveByteArrayTypeDescriptor
import org.hibernate.type.descriptor.sql.BlobTypeDescriptor
import org.hibernate.type.descriptor.sql.VarbinaryTypeDescriptor
import java.lang.management.ManagementFactory
import java.sql.Connection
import java.util.ServiceLoader
import javax.management.ObjectName
import javax.persistence.AttributeConverter
@ -30,35 +21,38 @@ class HibernateConfiguration(
schemas: Set<MappedSchema>,
private val databaseConfig: DatabaseConfig,
private val attributeConverters: Collection<AttributeConverter<*, *>>,
private val jdbcUrl: String,
jdbcUrl: String,
cacheFactory: NamedCacheFactory,
val customClassLoader: ClassLoader? = null
) {
companion object {
private val logger = contextLogger()
// register custom converters
fun buildHibernateMetadata(metadataBuilder: MetadataBuilder, jdbcUrl:String, attributeConverters: Collection<AttributeConverter<*, *>>): Metadata {
metadataBuilder.run {
attributeConverters.forEach { applyAttributeConverter(it) }
// Register a tweaked version of `org.hibernate.type.MaterializedBlobType` that truncates logged messages.
// to avoid OOM when large blobs might get logged.
applyBasicType(CordaMaterializedBlobType, CordaMaterializedBlobType.name)
applyBasicType(CordaWrapperBinaryType, CordaWrapperBinaryType.name)
// Will be used in open core
fun buildHibernateMetadata(metadataBuilder: MetadataBuilder, jdbcUrl: String, attributeConverters: Collection<AttributeConverter<*, *>>): Metadata {
val sff = findSessionFactoryFactory(jdbcUrl, null)
return sff.buildHibernateMetadata(metadataBuilder, attributeConverters)
}
// Create a custom type that will map a blob to byteA in postgres and as a normal blob for all other dbms.
// This is required for the Checkpoints as a workaround for the issue that postgres has on azure.
if (jdbcUrl.contains(":postgresql:", ignoreCase = true)) {
applyBasicType(MapBlobToPostgresByteA, MapBlobToPostgresByteA.name)
} else {
applyBasicType(MapBlobToNormalBlob, MapBlobToNormalBlob.name)
}
private fun findSessionFactoryFactory(jdbcUrl: String, customClassLoader: ClassLoader?): CordaSessionFactoryFactory {
val serviceLoader = if (customClassLoader != null)
ServiceLoader.load(CordaSessionFactoryFactory::class.java, customClassLoader)
else
ServiceLoader.load(CordaSessionFactoryFactory::class.java)
return build()
val sessionFactories = serviceLoader.filter { it.canHandleDatabase(jdbcUrl) }
when (sessionFactories.size) {
0 -> throw HibernateConfigException("Failed to find a SessionFactoryFactory to handle $jdbcUrl " +
"- factories present for ${serviceLoader.map { it.databaseType }}")
1 -> return sessionFactories.single()
else -> throw HibernateConfigException("Found several SessionFactoryFactory classes to handle $jdbcUrl " +
"- classes ${sessionFactories.map { it.javaClass.canonicalName }}")
}
}
}
val sessionFactoryFactory = findSessionFactoryFactory(jdbcUrl, customClassLoader)
private val sessionFactories = cacheFactory.buildNamed<Set<MappedSchema>, SessionFactory>(Caffeine.newBuilder(), "HibernateConfiguration_sessionFactories")
val sessionFactoryForRegisteredSchemas = schemas.let {
@ -70,35 +64,7 @@ class HibernateConfiguration(
fun sessionFactoryForSchemas(key: Set<MappedSchema>): SessionFactory = sessionFactories.get(key, ::makeSessionFactoryForSchemas)!!
private fun makeSessionFactoryForSchemas(schemas: Set<MappedSchema>): SessionFactory {
logger.info("Creating session factory for schemas: $schemas")
val serviceRegistry = BootstrapServiceRegistryBuilder().build()
val metadataSources = MetadataSources(serviceRegistry)
val hbm2dll: String =
if(databaseConfig.initialiseSchema && databaseConfig.initialiseAppSchema == SchemaInitializationType.UPDATE) {
"update"
} else if((!databaseConfig.initialiseSchema && databaseConfig.initialiseAppSchema == SchemaInitializationType.UPDATE)
|| databaseConfig.initialiseAppSchema == SchemaInitializationType.VALIDATE) {
"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.
val config = Configuration(metadataSources).setProperty("hibernate.connection.provider_class", NodeDatabaseConnectionProvider::class.java.name)
.setProperty("hibernate.format_sql", "true")
.setProperty("hibernate.hbm2ddl.auto", hbm2dll)
.setProperty("javax.persistence.validation.mode", "none")
.setProperty("hibernate.connection.isolation", databaseConfig.transactionIsolationLevel.jdbcValue.toString())
schemas.forEach { schema ->
// TODO: require mechanism to set schemaOptions (databaseSchema, tablePrefix) which are not global to session
schema.mappedTypes.forEach { config.addAnnotatedClass(it) }
}
val sessionFactory = buildSessionFactory(config, metadataSources, customClassLoader)
logger.info("Created session factory for schemas: $schemas")
val sessionFactory = sessionFactoryFactory.makeSessionFactoryForSchemas(databaseConfig, schemas, customClassLoader, attributeConverters)
// export Hibernate JMX statistics
if (databaseConfig.exportHibernateJMXStatistics)
@ -123,27 +89,6 @@ class HibernateConfiguration(
}
}
private fun buildSessionFactory(config: Configuration, metadataSources: MetadataSources, customClassLoader: ClassLoader?): SessionFactory {
config.standardServiceRegistryBuilder.applySettings(config.properties)
if (customClassLoader != null) {
config.standardServiceRegistryBuilder.addService(
ClassLoaderService::class.java,
ClassLoaderServiceImpl(customClassLoader))
}
@Suppress("DEPRECATION")
val metadataBuilder = metadataSources.getMetadataBuilder(config.standardServiceRegistryBuilder.build())
val metadata = buildHibernateMetadata(metadataBuilder, jdbcUrl, attributeConverters)
return metadata.sessionFactoryBuilder.run {
allowOutOfTransactionUpdateOperations(true)
applySecondLevelCacheSupport(false)
applyQueryCacheSupport(false)
enableReleaseResourcesOnCloseEnabled(true)
build()
}
}
// Supply Hibernate with connections from our underlying Exposed database integration. Only used
// during schema creation / update.
class NodeDatabaseConnectionProvider : ConnectionProvider {
@ -168,55 +113,5 @@ class HibernateConfiguration(
override fun isUnwrappableAs(unwrapType: Class<*>?): Boolean = unwrapType == NodeDatabaseConnectionProvider::class.java
}
// A tweaked version of `org.hibernate.type.MaterializedBlobType` that truncates logged messages. Also logs in hex.
object CordaMaterializedBlobType : AbstractSingleColumnStandardBasicType<ByteArray>(BlobTypeDescriptor.DEFAULT, CordaPrimitiveByteArrayTypeDescriptor) {
override fun getName(): String {
return "materialized_blob"
}
}
// A tweaked version of `org.hibernate.type.descriptor.java.PrimitiveByteArrayTypeDescriptor` that truncates logged messages.
private object CordaPrimitiveByteArrayTypeDescriptor : PrimitiveByteArrayTypeDescriptor() {
private const val LOG_SIZE_LIMIT = 1024
override fun extractLoggableRepresentation(value: ByteArray?): String {
return if (value == null) {
super.extractLoggableRepresentation(value)
} else {
if (value.size <= LOG_SIZE_LIMIT) {
"[size=${value.size}, value=${value.toHexString()}]"
} else {
"[size=${value.size}, value=${value.copyOfRange(0, LOG_SIZE_LIMIT).toHexString()}...truncated...]"
}
}
}
}
// A tweaked version of `org.hibernate.type.WrapperBinaryType` that deals with ByteArray (java primitive byte[] type).
object CordaWrapperBinaryType : AbstractSingleColumnStandardBasicType<ByteArray>(VarbinaryTypeDescriptor.INSTANCE, PrimitiveByteArrayTypeDescriptor.INSTANCE) {
override fun getRegistrationKeys(): Array<String> {
return arrayOf(name, "ByteArray", ByteArray::class.java.name)
}
override fun getName(): String {
return "corda-wrapper-binary"
}
}
// Maps to a byte array on postgres.
object MapBlobToPostgresByteA : AbstractSingleColumnStandardBasicType<ByteArray>(VarbinaryTypeDescriptor.INSTANCE, PrimitiveByteArrayTypeDescriptor.INSTANCE) {
override fun getRegistrationKeys(): Array<String> {
return arrayOf(name, "ByteArray", ByteArray::class.java.name)
}
override fun getName(): String {
return "corda-blob"
}
}
object MapBlobToNormalBlob : MaterializedBlobType() {
override fun getName(): String {
return "corda-blob"
}
}
fun getExtraConfiguration(key: String ) = sessionFactoryFactory.getExtraConfiguration(key)
}

View File

@ -0,0 +1,138 @@
package net.corda.nodeapi.internal.persistence.factory
import net.corda.core.schemas.MappedSchema
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 org.hibernate.SessionFactory
import org.hibernate.boot.Metadata
import org.hibernate.boot.MetadataBuilder
import org.hibernate.boot.MetadataSources
import org.hibernate.boot.registry.BootstrapServiceRegistryBuilder
import org.hibernate.boot.registry.classloading.internal.ClassLoaderServiceImpl
import org.hibernate.boot.registry.classloading.spi.ClassLoaderService
import org.hibernate.cfg.Configuration
import org.hibernate.type.AbstractSingleColumnStandardBasicType
import org.hibernate.type.MaterializedBlobType
import org.hibernate.type.descriptor.java.PrimitiveByteArrayTypeDescriptor
import org.hibernate.type.descriptor.sql.BlobTypeDescriptor
import org.hibernate.type.descriptor.sql.VarbinaryTypeDescriptor
import javax.persistence.AttributeConverter
abstract class BaseSessionFactoryFactory : CordaSessionFactoryFactory {
companion object {
private val logger = contextLogger()
}
open fun buildHibernateConfig(databaseConfig: DatabaseConfig, metadataSources: MetadataSources): Configuration {
// 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.hbm2ddl.auto", "validate")
}
override fun buildHibernateMetadata(metadataBuilder: MetadataBuilder, attributeConverters: Collection<AttributeConverter<*, *>>): Metadata {
return metadataBuilder.run {
attributeConverters.forEach { applyAttributeConverter(it) }
// Register a tweaked version of `org.hibernate.type.MaterializedBlobType` that truncates logged messages.
// to avoid OOM when large blobs might get logged.
applyBasicType(CordaMaterializedBlobType, CordaMaterializedBlobType.name)
applyBasicType(CordaWrapperBinaryType, CordaWrapperBinaryType.name)
applyBasicType(MapBlobToNormalBlob, MapBlobToNormalBlob.name)
build()
}
}
fun buildSessionFactory(
config: Configuration,
metadataSources: MetadataSources,
customClassLoader: ClassLoader?,
attributeConverters: Collection<AttributeConverter<*, *>>): SessionFactory {
config.standardServiceRegistryBuilder.applySettings(config.properties)
if (customClassLoader != null) {
config.standardServiceRegistryBuilder.addService(
ClassLoaderService::class.java,
ClassLoaderServiceImpl(customClassLoader))
}
@Suppress("DEPRECATION")
val metadataBuilder = metadataSources.getMetadataBuilder(config.standardServiceRegistryBuilder.build())
val metadata = buildHibernateMetadata(metadataBuilder, attributeConverters)
return metadata.sessionFactoryBuilder.run {
allowOutOfTransactionUpdateOperations(true)
applySecondLevelCacheSupport(false)
applyQueryCacheSupport(false)
enableReleaseResourcesOnCloseEnabled(true)
build()
}
}
final override fun makeSessionFactoryForSchemas(
databaseConfig: DatabaseConfig,
schemas: Set<MappedSchema>,
customClassLoader: ClassLoader?,
attributeConverters: Collection<AttributeConverter<*, *>>): SessionFactory {
logger.info("Creating session factory for schemas: $schemas")
val serviceRegistry = BootstrapServiceRegistryBuilder().build()
val metadataSources = MetadataSources(serviceRegistry)
val config = buildHibernateConfig(databaseConfig, metadataSources)
schemas.forEach { schema ->
schema.mappedTypes.forEach { config.addAnnotatedClass(it) }
}
val sessionFactory = buildSessionFactory(config, metadataSources, customClassLoader, attributeConverters)
logger.info("Created session factory for schemas: $schemas")
return sessionFactory
}
override fun getExtraConfiguration(key: String): Any? {
return null
}
// A tweaked version of `org.hibernate.type.WrapperBinaryType` that deals with ByteArray (java primitive byte[] type).
object CordaWrapperBinaryType : AbstractSingleColumnStandardBasicType<ByteArray>(VarbinaryTypeDescriptor.INSTANCE, PrimitiveByteArrayTypeDescriptor.INSTANCE) {
override fun getRegistrationKeys(): Array<String> {
return arrayOf(name, "ByteArray", ByteArray::class.java.name)
}
override fun getName(): String {
return "corda-wrapper-binary"
}
}
object MapBlobToNormalBlob : MaterializedBlobType() {
override fun getName(): String {
return "corda-blob"
}
}
// A tweaked version of `org.hibernate.type.descriptor.java.PrimitiveByteArrayTypeDescriptor` that truncates logged messages.
private object CordaPrimitiveByteArrayTypeDescriptor : PrimitiveByteArrayTypeDescriptor() {
private const val LOG_SIZE_LIMIT = 1024
override fun extractLoggableRepresentation(value: ByteArray?): String {
return if (value == null) {
super.extractLoggableRepresentation(value)
} else {
if (value.size <= LOG_SIZE_LIMIT) {
"[size=${value.size}, value=${value.toHexString()}]"
} else {
"[size=${value.size}, value=${value.copyOfRange(0, LOG_SIZE_LIMIT).toHexString()}...truncated...]"
}
}
}
}
// A tweaked version of `org.hibernate.type.MaterializedBlobType` that truncates logged messages. Also logs in hex.
object CordaMaterializedBlobType : AbstractSingleColumnStandardBasicType<ByteArray>(BlobTypeDescriptor.DEFAULT, CordaPrimitiveByteArrayTypeDescriptor) {
override fun getName(): String {
return "materialized_blob"
}
}
}

View File

@ -0,0 +1,20 @@
package net.corda.nodeapi.internal.persistence.factory
import net.corda.core.schemas.MappedSchema
import net.corda.nodeapi.internal.persistence.DatabaseConfig
import org.hibernate.SessionFactory
import org.hibernate.boot.Metadata
import org.hibernate.boot.MetadataBuilder
import javax.persistence.AttributeConverter
interface CordaSessionFactoryFactory {
val databaseType: String
fun canHandleDatabase(jdbcUrl: String): Boolean
fun makeSessionFactoryForSchemas(
databaseConfig: DatabaseConfig,
schemas: Set<MappedSchema>,
customClassLoader: ClassLoader?,
attributeConverters: Collection<AttributeConverter<*, *>>): SessionFactory
fun getExtraConfiguration(key: String): Any?
fun buildHibernateMetadata(metadataBuilder: MetadataBuilder, attributeConverters: Collection<AttributeConverter<*, *>>): Metadata
}

View File

@ -0,0 +1,27 @@
package net.corda.nodeapi.internal.persistence.factory
import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.nodeapi.internal.persistence.SchemaInitializationType
import org.hibernate.boot.MetadataSources
import org.hibernate.cfg.Configuration
class H2SessionFactoryFactory : BaseSessionFactoryFactory() {
override fun canHandleDatabase(jdbcUrl: String): Boolean = jdbcUrl.startsWith("jdbc:h2:")
override val databaseType: String = "H2"
override fun buildHibernateConfig(databaseConfig: DatabaseConfig, metadataSources: MetadataSources): Configuration {
val config = super.buildHibernateConfig(databaseConfig, metadataSources)
val hbm2dll: String =
if (databaseConfig.initialiseSchema && databaseConfig.initialiseAppSchema == SchemaInitializationType.UPDATE) {
"update"
} else if ((!databaseConfig.initialiseSchema && databaseConfig.initialiseAppSchema == SchemaInitializationType.UPDATE)
|| databaseConfig.initialiseAppSchema == SchemaInitializationType.VALIDATE) {
"validate"
} else {
"none"
}
config.setProperty("hibernate.hbm2ddl.auto", hbm2dll)
return config
}
}

View File

@ -0,0 +1,41 @@
package net.corda.nodeapi.internal.persistence.factory
import org.hibernate.boot.Metadata
import org.hibernate.boot.MetadataBuilder
import org.hibernate.type.AbstractSingleColumnStandardBasicType
import org.hibernate.type.descriptor.java.PrimitiveByteArrayTypeDescriptor
import org.hibernate.type.descriptor.sql.VarbinaryTypeDescriptor
import javax.persistence.AttributeConverter
class PostgresSessionFactoryFactory : BaseSessionFactoryFactory() {
override fun buildHibernateMetadata(metadataBuilder: MetadataBuilder, attributeConverters: Collection<AttributeConverter<*, *>>): Metadata {
return metadataBuilder.run {
attributeConverters.forEach { applyAttributeConverter(it) }
// Register a tweaked version of `org.hibernate.type.MaterializedBlobType` that truncates logged messages.
// to avoid OOM when large blobs might get logged.
applyBasicType(CordaMaterializedBlobType, CordaMaterializedBlobType.name)
applyBasicType(CordaWrapperBinaryType, CordaWrapperBinaryType.name)
// Create a custom type that will map a blob to byteA in postgres
// This is required for the Checkpoints as a workaround for the issue that postgres has on azure.
applyBasicType(MapBlobToPostgresByteA, MapBlobToPostgresByteA.name)
build()
}
}
override fun canHandleDatabase(jdbcUrl: String): Boolean = jdbcUrl.contains(":postgresql:")
// Maps to a byte array on postgres.
object MapBlobToPostgresByteA : AbstractSingleColumnStandardBasicType<ByteArray>(VarbinaryTypeDescriptor.INSTANCE, PrimitiveByteArrayTypeDescriptor.INSTANCE) {
override fun getRegistrationKeys(): Array<String> {
return arrayOf(name, "ByteArray", ByteArray::class.java.name)
}
override fun getName(): String {
return "corda-blob"
}
}
override val databaseType: String = "PostgreSQL"
}

View File

@ -0,0 +1,2 @@
net.corda.nodeapi.internal.persistence.factory.H2SessionFactoryFactory
net.corda.nodeapi.internal.persistence.factory.PostgresSessionFactoryFactory

View File

@ -0,0 +1,26 @@
package net.corda.nodeapi.internal.persistence
import com.nhaarman.mockito_kotlin.mock
import net.corda.core.internal.NamedCacheFactory
import org.junit.Assert
import org.junit.Test
class HibernateConfigurationFactoryLoadingTest {
@Test(timeout=300_000)
fun checkErrorMessageForMissingFactory() {
val jdbcUrl = "jdbc:madeUpNonense:foobar.com:1234"
val presentFactories = listOf("H2", "PostgreSQL")
try {
val cacheFactory = mock<NamedCacheFactory>()
HibernateConfiguration(
emptySet(),
DatabaseConfig(),
emptyList(),
jdbcUrl,
cacheFactory)
Assert.fail("Expected exception not thrown")
} catch (e: HibernateConfigException) {
Assert.assertEquals("Failed to find a SessionFactoryFactory to handle $jdbcUrl - factories present for ${presentFactories}", e.message)
}
}
}