libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

DbPool.kt (3122B)


      1 /*
      2  * This file is part of LibEuFin.
      3  * Copyright (C) 2024-2025 Taler Systems S.A.
      4  *
      5  * LibEuFin is free software; you can redistribute it and/or modify
      6  * it under the terms of the GNU Affero General Public License as
      7  * published by the Free Software Foundation; either version 3, or
      8  * (at your option) any later version.
      9  *
     10  * LibEuFin is distributed in the hope that it will be useful, but
     11  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
     12  * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General
     13  * Public License for more details.
     14  *
     15  * You should have received a copy of the GNU Affero General Public
     16  * License along with LibEuFin; see the file COPYING.  If not, see
     17  * <http://www.gnu.org/licenses/>
     18  */
     19 
     20 package tech.libeufin.common.db
     21 
     22 import com.zaxxer.hikari.HikariConfig
     23 import com.zaxxer.hikari.HikariDataSource
     24 import kotlinx.coroutines.Dispatchers
     25 import kotlinx.coroutines.withContext
     26 import org.postgresql.jdbc.PgConnection
     27 import tech.libeufin.common.MIN_VERSION
     28 import java.sql.PreparedStatement
     29 
     30 open class DbPool(cfg: DatabaseConfig, schema: String) : java.io.Closeable {
     31     val pgSource = pgDataSource(cfg.dbConnStr)
     32     private val pool: HikariDataSource
     33 
     34     init {
     35         val config = HikariConfig()
     36         config.dataSource = pgSource
     37         config.schema = schema.replace("-", "_")
     38         config.transactionIsolation = "TRANSACTION_SERIALIZABLE"
     39         pool = HikariDataSource(config)
     40         pool.connection.use { con ->
     41             val meta = con.metaData
     42             val majorVersion = meta.databaseMajorVersion
     43             val minorVersion = meta.databaseMinorVersion
     44             require(majorVersion >= MIN_VERSION) {
     45                 "postgres version must be at least $MIN_VERSION.0 got $majorVersion.$minorVersion"
     46             }
     47             checkMigrations(con.unwrap(PgConnection::class.java), cfg, schema)
     48         }
     49     }
     50 
     51     /** Executes a query with automatic retry on serialization errors */
     52     suspend fun <R> serializable(query: String, lambda: TalerStatement.() -> R): R = conn { conn ->
     53         // We could explicitly tell Postgres when a request is read-only,
     54         // but the performance improvement isn't obvious, it doesn't prevent
     55         // stored procedures from modifying the database and it adds a 
     56         // round-trip during configuration
     57 
     58         conn.withStatement(query) {
     59             retrySerializationError { lambda() }
     60         }
     61     }
     62 
     63     /** Executes a transaction with automatic retry on serialization errors */
     64     suspend fun <R> serializableTransaction(transaction: (PgConnection) -> R): R = conn { conn ->
     65         retrySerializationError { 
     66             conn.transaction(transaction)
     67         }
     68     }
     69 
     70     /** Run db logic using a connection from the pool */
     71     suspend fun <R> conn(lambda: suspend (PgConnection) -> R): R {
     72         // Use a coroutine dispatcher that we can block as JDBC API is blocking
     73         return withContext(Dispatchers.IO) {
     74             pool.connection.use { lambda(it.unwrap(PgConnection::class.java)) }
     75         }
     76     }
     77 
     78     override fun close() {
     79         pool.close()
     80     }
     81 }