package com.osglab.account.config import com.zaxxer.hikari.HikariConfig import com.zaxxer.hikari.HikariDataSource import kotlinx.coroutines.Dispatchers import org.flywaydb.core.Flyway import org.jetbrains.exposed.v1.jdbc.Database import org.jetbrains.exposed.v1.jdbc.transactions.suspendTransaction import java.sql.DriverManager class DatabaseFactory( private val config: DatabaseConfig, ) : AutoCloseable { private val dataSourceDelegate = lazy(::createDataSource) private val dataSource: HikariDataSource by dataSourceDelegate val database: Database by lazy { Flyway.configure() .dataSource( config.jdbcUrl, config.migrationUsername, config.migrationPassword, ) .validateMigrationNaming(true) .load() .migrate() Database.connect(dataSource) } suspend fun query(block: suspend () -> T): T = kotlinx.coroutines.withContext(Dispatchers.IO) { suspendTransaction(database) { block() } } suspend fun isReady(): Boolean = kotlinx.coroutines.withContext(Dispatchers.IO) { runCatching { // Initializing `database` also validates and applies Flyway migrations. database dataSource.connection.use { connection -> connection.prepareStatement("SELECT 1").use { statement -> statement.executeQuery().use { result -> check(result.next() && result.getInt(1) == 1) } } } }.isSuccess } /** * Uses a dedicated physical connection because MySQL named locks are * connection-scoped. The protected block may use normal repository * transactions without exhausting the Hikari pool. */ suspend fun withMysqlNamedLock( name: String, timeoutSeconds: Int, block: suspend () -> T, ): T = kotlinx.coroutines.withContext(Dispatchers.IO) { require(name.isNotBlank() && name.length <= 64) require(timeoutSeconds in 1..60) DriverManager.getConnection(config.jdbcUrl, config.username, config.password).use { connection -> val acquired = connection.prepareStatement("SELECT GET_LOCK(?, ?)").use { statement -> statement.setString(1, name) statement.setInt(2, timeoutSeconds) statement.executeQuery().use { result -> result.next() && result.getInt(1) == 1 } } if (!acquired) throw IllegalStateException("Timed out acquiring database named lock") try { block() } finally { runCatching { connection.prepareStatement("SELECT RELEASE_LOCK(?)").use { statement -> statement.setString(1, name) statement.executeQuery().close() } } } } } override fun close() { if (dataSourceDelegate.isInitialized()) { dataSourceDelegate.value.close() } } private fun createDataSource(): HikariDataSource = HikariDataSource( HikariConfig().apply { jdbcUrl = config.jdbcUrl username = config.username password = config.password maximumPoolSize = config.maximumPoolSize minimumIdle = 1 connectionTimeout = 10_000 validationTimeout = 5_000 idleTimeout = 600_000 maxLifetime = 1_800_000 isAutoCommit = false transactionIsolation = "TRANSACTION_READ_COMMITTED" connectionInitSql = "SET time_zone = '+00:00'" poolName = "osg-account-db" }, ) }