Skip to content

Commit a0f9683

Browse files
authored
Merge pull request #2739 from hongwei1/feature/botswanaBranch
Feature/Unified Doobie's connection management with Lift
2 parents c77a401 + 6875001 commit a0f9683

File tree

1 file changed

+74
-63
lines changed

1 file changed

+74
-63
lines changed

obp-api/src/main/scala/code/api/util/DoobieTransactor.scala

Lines changed: 74 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package code.api.util
22

3-
import cats.effect.{IO, Resource}
3+
import cats.effect.IO
44
import cats.effect.unsafe.implicits.global
5-
import com.zaxxer.hikari.HikariConfig
5+
import code.util.Helper.MdcLoggable
66
import doobie._
7-
import doobie.hikari.HikariTransactor
87
import doobie.implicits._
8+
import doobie.util.transactor.Strategy
9+
import net.liftweb.common.Full
10+
import net.liftweb.db.DB
911

1012
import scala.concurrent.{ExecutionContext, Future}
1113

@@ -16,16 +18,22 @@ import scala.concurrent.{ExecutionContext, Future}
1618
* This handles all JDBC types correctly, including SQL Server's NVARCHAR (type -9)
1719
* which Lift's DB.runQuery doesn't handle.
1820
*
19-
* IMPORTANT: This uses a SEPARATE HikariCP connection pool from Lift's pool.
20-
* This ensures complete isolation and safety - Doobie manages its own connections
21-
* without any interference with Lift's connection management.
21+
* TRANSACTION UNIFICATION:
22+
* When called within a Lift HTTP request context, Doobie uses the SAME Connection
23+
* that Lift is holding for the current request transaction (via Transactor.fromConnection).
24+
* This means Doobie queries participate in Lift's transaction boundary:
25+
* - Same connection, same transaction, same commit/rollback
26+
* - Doobie can see uncommitted Lift writes (same session)
27+
* - If Lift rolls back, Doobie's operations are also rolled back
28+
*
29+
* When called outside a Lift request context (e.g., background tasks, schedulers),
30+
* falls back to Lift's shared HikariCP connection pool via Transactor.fromDataSource.
2231
*
2332
* Benefits over DBUtil.runQuery:
2433
* - Type-safe query results via case classes
2534
* - Type-safe parameters (no SQL injection risk)
2635
* - Proper JDBC type handling for all databases
2736
* - Composable queries using cats-effect IO
28-
* - Complete isolation from Lift's connection pool
2937
*
3038
* Usage:
3139
* {{{
@@ -45,96 +53,99 @@ import scala.concurrent.{ExecutionContext, Future}
4553
* val result: List[TopApi] = DoobieUtil.runQuery(query)
4654
* }}}
4755
*/
48-
object DoobieUtil {
56+
object DoobieUtil extends MdcLoggable {
4957

5058
/**
51-
* Lazy-initialized HikariCP transactor for Doobie.
52-
*
53-
* This creates a separate connection pool from Lift's pool, ensuring
54-
* complete isolation and safety. The pool is initialized on first use
55-
* and kept alive for the lifetime of the application.
59+
* Fallback transactor that shares Lift's HikariCP connection pool.
60+
* Used when no Lift request context is available (background tasks, schedulers).
61+
* Strategy.void: Doobie will not call setAutoCommit/commit/rollback.
5662
*/
57-
private lazy val transactor: Transactor[IO] = {
58-
val (dbUrl, dbUser, dbPassword) = DBUtil.getDbConnectionParameters
59-
60-
val hikariConfig = new HikariConfig()
61-
hikariConfig.setJdbcUrl(dbUrl)
62-
hikariConfig.setUsername(dbUser)
63-
hikariConfig.setPassword(dbPassword)
64-
65-
// Pool configuration - conservative settings for safety
66-
hikariConfig.setPoolName("doobie-pool")
67-
hikariConfig.setMaximumPoolSize(
68-
APIUtil.getPropsAsIntValue("doobie.hikari.maximumPoolSize", 10)
69-
)
70-
hikariConfig.setMinimumIdle(
71-
APIUtil.getPropsAsIntValue("doobie.hikari.minimumIdle", 2)
72-
)
73-
hikariConfig.setConnectionTimeout(
74-
APIUtil.getPropsAsLongValue("doobie.hikari.connectionTimeout", 30000L)
75-
)
76-
hikariConfig.setIdleTimeout(
77-
APIUtil.getPropsAsLongValue("doobie.hikari.idleTimeout", 600000L)
78-
)
79-
hikariConfig.setMaxLifetime(
80-
APIUtil.getPropsAsLongValue("doobie.hikari.maxLifetime", 1800000L)
63+
private lazy val fallbackTransactor: Transactor[IO] = {
64+
val liftDataSource = APIUtil.vendor.HikariDatasource.ds
65+
logger.info("DoobieUtil: Initialized fallback transactor sharing Lift's HikariCP pool")
66+
val xa = Transactor.fromDataSource[IO].apply(
67+
liftDataSource,
68+
ExecutionContext.global
8169
)
70+
xa.copy(strategy0 = Strategy.void)
71+
}
8272

83-
// Set driver class based on database type
84-
if (dbUrl.contains("sqlserver")) {
85-
hikariConfig.setDriverClassName("com.microsoft.sqlserver.jdbc.SQLServerDriver")
86-
} else if (dbUrl.contains("postgresql")) {
87-
hikariConfig.setDriverClassName("org.postgresql.Driver")
88-
} else if (dbUrl.contains("mysql")) {
89-
hikariConfig.setDriverClassName("com.mysql.cj.jdbc.Driver")
90-
} else if (dbUrl.contains("h2")) {
91-
hikariConfig.setDriverClassName("org.h2.Driver")
92-
} else if (dbUrl.contains("oracle")) {
93-
hikariConfig.setDriverClassName("oracle.jdbc.OracleDriver")
94-
}
95-
// For other databases, HikariCP will try to auto-detect the driver
73+
/**
74+
* Create a transactor that wraps an existing JDBC Connection.
75+
* Strategy.void ensures Doobie does not interfere with Lift's transaction management.
76+
*/
77+
private def transactorFromConnection(conn: java.sql.Connection): Transactor[IO] = {
78+
val xa = Transactor.fromConnection[IO].apply(conn, None)
79+
xa.copy(strategy0 = Strategy.void)
80+
}
9681

97-
// Create the transactor - this allocates the pool immediately
98-
// We use unsafeRunSync here because we need a stable, long-lived transactor
99-
HikariTransactor.fromHikariConfig[IO](hikariConfig)
100-
.allocated
101-
.map(_._1) // Get the transactor, discard the finalizer (pool lives for app lifetime)
102-
.unsafeRunSync()
82+
/**
83+
* Try to get the current Lift request's Connection.
84+
* Uses DB.currentConnection which peeks at the DynoVar without
85+
* triggering reference counting or creating a new connection.
86+
* Returns Some(connection) if inside a Lift HTTP request context,
87+
* None otherwise (background tasks, schedulers, tests without request context).
88+
*/
89+
private def liftCurrentConnection: Option[java.sql.Connection] = {
90+
// DB.currentConnection returns Box[SuperConnection]
91+
// SuperConnection has implicit conversion to java.sql.Connection
92+
DB.currentConnection match {
93+
case Full(superConn) =>
94+
val conn: java.sql.Connection = superConn.connection
95+
if (!conn.isClosed) Some(conn) else None
96+
case _ => None
97+
}
10398
}
10499

105100
/**
106-
* Run a Doobie query synchronously using the dedicated Doobie connection pool.
101+
* Run a Doobie query synchronously, sharing Lift's transaction when available.
107102
*
108-
* This uses a completely separate HikariCP pool from Lift's pool,
109-
* ensuring no interference between Doobie and Lift's connection management.
103+
* When called within a Lift HTTP request context:
104+
* - Uses the SAME Connection that Lift holds for the current request
105+
* - Doobie query participates in Lift's transaction (same commit/rollback)
106+
* - Can see uncommitted Lift writes (same database session)
107+
*
108+
* When called outside a Lift request context (background tasks, schedulers):
109+
* - Falls back to Lift's shared HikariCP pool (separate connection)
110110
*
111111
* @param query The Doobie ConnectionIO query to execute
112112
* @return The query result
113113
*/
114114
def runQuery[A](query: ConnectionIO[A]): A = {
115-
query.transact(transactor).unsafeRunSync()
115+
liftCurrentConnection match {
116+
case Some(conn) =>
117+
// Inside Lift request: use the same connection for transaction unification
118+
query.transact(transactorFromConnection(conn)).unsafeRunSync()
119+
case None =>
120+
// Outside Lift request: fallback to shared pool
121+
logger.debug("DoobieUtil.runQuery: No Lift request context, using fallback pool transactor")
122+
query.transact(fallbackTransactor).unsafeRunSync()
123+
}
116124
}
117125

118126
/**
119127
* Run a Doobie query asynchronously, returning a Future.
128+
* Note: async queries always use the fallback pool transactor because
129+
* Lift's request connection may not be available on a different thread.
120130
*
121131
* @param query The Doobie ConnectionIO query to execute
122132
* @param ec ExecutionContext for the Future
123133
* @return Future containing the query result
124134
*/
125135
def runQueryAsync[A](query: ConnectionIO[A])(implicit ec: ExecutionContext): Future[A] = {
126-
query.transact(transactor).unsafeToFuture()
136+
query.transact(fallbackTransactor).unsafeToFuture()
127137
}
128138

129139
/**
130140
* Run a Doobie query and return an IO.
131-
* Useful when you want to compose with other cats-effect operations.
141+
* Note: IO queries always use the fallback pool transactor because
142+
* the IO may be evaluated outside the Lift request context.
132143
*
133144
* @param query The Doobie ConnectionIO query to execute
134145
* @return IO containing the query result
135146
*/
136147
def runQueryIO[A](query: ConnectionIO[A]): IO[A] = {
137-
query.transact(transactor)
148+
query.transact(fallbackTransactor)
138149
}
139150

140151
/**

0 commit comments

Comments
 (0)