0af35d44f4
Provide the production foundation for Apple identity, immutable credits, referrals, integrity checks, managed providers, and hardened Docker deployment.
107 lines
3.8 KiB
Kotlin
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"
|
|
},
|
|
)
|
|
}
|