Files
OSGAccountServer/src/main/kotlin/com/osglab/account/config/DatabaseFactory.kt
T
Rocky 0af35d44f4 Establish secure account and managed AI backend
Provide the production foundation for Apple identity, immutable credits, referrals, integrity checks, managed providers, and hardened Docker deployment.
2026-08-16 14:46:23 +08:00

107 lines
3.8 KiB
Kotlin

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 <T> 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 <T> 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"
},
)
}