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.
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
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"
|
||||
},
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user