Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds HikariCP (#639) #704

Merged
merged 7 commits into from
Aug 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ lazy val root = project
mysql,
oracle,
postgres,
sqlserver
sqlserver,
jdbc_hikaricp
)

lazy val core = crossProject(JSPlatform, JVMPlatform)
Expand Down Expand Up @@ -131,6 +132,23 @@ lazy val jdbc = project
.settings(testFrameworks += new TestFramework("zio.test.sbt.ZTestFramework"))
.dependsOn(core.jvm)

lazy val jdbc_hikaricp = project
.in(file("jdbc-hikaricp"))
.settings(stdSettings("zio-sql-jdbc-hickaricp"))
.settings(buildInfoSettings("zio.sql.jdbc-hickaricp"))
.settings(
libraryDependencies ++= Seq(
"com.zaxxer" % "HikariCP" % "5.0.1",
"dev.zio" %% "zio-test" % zioVersion % Test,
"dev.zio" %% "zio-test-sbt" % zioVersion % Test,
"org.testcontainers" % "mysql" % testcontainersVersion % Test,
"mysql" % "mysql-connector-java" % "8.0.29" % Test,
"com.dimafeng" %% "testcontainers-scala-mysql" % testcontainersScalaVersion % Test
)
)
.settings(testFrameworks += new TestFramework("zio.test.sbt.ZTestFramework"))
.dependsOn(jdbc)

lazy val mysql = project
.in(file("mysql"))
.dependsOn(jdbc % "compile->compile;test->test")
Expand Down
35 changes: 35 additions & 0 deletions jdbc-hikaricp/src/main/scala/zio/sql/HikariConnectionPool.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package zio.sql
import com.zaxxer.hikari.{ HikariConfig, HikariDataSource }
import zio.{ Scope, ZIO, ZLayer }

import java.sql.{ Connection, SQLException }

class HikariConnectionPool private (hikariDataSource: HikariDataSource) extends ConnectionPool {

private[sql] val dataSource = hikariDataSource

/**
* Retrieves a JDBC java.sql.Connection as a [[ZIO[Scope, Exception, Connection]]] resource.
* The managed resource will safely acquire and release the connection, and
* may be interrupted or timed out if necessary.
*/
override def connection: ZIO[Scope, Exception, Connection] =
ZIO.acquireRelease(ZIO.attemptBlocking(hikariDataSource.getConnection).refineToOrDie[SQLException])(con =>
ZIO.attemptBlocking(hikariDataSource.evictConnection(con)).orDie
)
}

object HikariConnectionPool {

private[sql] def initDataSource(config: HikariConfig): ZIO[Scope, Throwable, HikariDataSource] =
ZIO.acquireRelease(ZIO.attemptBlocking(new HikariDataSource(config)))(ds => ZIO.attemptBlocking(ds.close()).orDie)

val live: ZLayer[HikariConnectionPoolConfig, Throwable, HikariConnectionPool] =
ZLayer.scoped {
for {
config <- ZIO.service[HikariConnectionPoolConfig]
dataSource <- initDataSource(config.toHikariConfig)
pool = new HikariConnectionPool(dataSource)
} yield pool
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package zio.sql

import com.zaxxer.hikari.HikariConfig

/**
* Configuration information for the connection pool.
*
* @param url The JDBC connection string.
* @param properties JDBC connection properties (username / password could go here).
* @param poolSize The size of the pool.
* @param connectionTimeout Maximum number of milliseconds that a client will wait for a connection from the pool.
* If this time is exceeded without a connection becoming available, a SQLException will be thrown from javax.sql.DataSource.getConnection().
* @param idleTimeout This property controls the maximum amount of time (in milliseconds) that a connection is allowed to sit idle in the pool.
* Whether a connection is retired as idle or not is subject to a maximum variation of +30 seconds, and average variation of +15 seconds.
* A connection will never be retired as idle before this timeout. A value of 0 means that idle connections are never removed from the pool.
* @param initializationFailTimeout the number of milliseconds before the
* pool initialization fails, or 0 to validate connection setup but continue with
* pool start, or less than zero to skip all initialization checks and start the
* pool without delay.
* @param maxLifetime This property controls the maximum lifetime of a connection in the pool.
* When a connection reaches this timeout, even if recently used, it will be retired from the pool.
* An in-use connection will never be retired, only when it is idle will it be removed. Should be bigger then 30000
* @param minimumIdle The property controls the minimum number of idle connections that HikariCP tries to maintain in the pool, including both idle and in-use connections.
* If the idle connections dip below this value, HikariCP will make a best effort to restore them quickly and efficiently.
* @param connectionInitSql the SQL to execute on new connections
* Set the SQL string that will be executed on all new connections when they are
* created, before they are added to the pool. If this query fails, it will be
* treated as a failed connection attempt.
*/
final case class HikariConnectionPoolConfig(
url: String,
userName: String,
password: String,
poolSize: Int = 10,
autoCommit: Boolean = true,
connectionTimeout: Option[Long] = None,
idleTimeout: Option[Long] = None,
initializationFailTimeout: Option[Long] = None,
maxLifetime: Option[Long] = None,
minimumIdle: Option[Int] = None,
connectionInitSql: Option[String] = None
) {
private[sql] def toHikariConfig = {
val hikariConfig = new HikariConfig()
hikariConfig.setJdbcUrl(this.url)
hikariConfig.setAutoCommit(this.autoCommit)
hikariConfig.setMaximumPoolSize(this.poolSize)
hikariConfig.setUsername(userName)
hikariConfig.setPassword(password)
connectionTimeout.foreach(hikariConfig.setConnectionTimeout)
idleTimeout.foreach(hikariConfig.setIdleTimeout)
initializationFailTimeout.foreach(hikariConfig.setInitializationFailTimeout)
maxLifetime.foreach(hikariConfig.setMaxLifetime)
minimumIdle.foreach(hikariConfig.setMinimumIdle)
connectionInitSql.foreach(hikariConfig.setConnectionInitSql)
hikariConfig
}
}
131 changes: 131 additions & 0 deletions jdbc-hikaricp/src/test/scala/zio/sql/HikariConnectionPoolSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package zio.sql

import zio.test.TestAspect.{ sequential, timeout, withLiveClock }
import zio.test.{ TestEnvironment, _ }
import zio.{ durationInt, ZIO, ZLayer }

object HikariConnectionPoolSpec extends ZIOSpecDefault {

val mySqlConfigLayer: ZLayer[Any, Throwable, MySqlConfig] =
ZLayer.scoped {
MySqlTestContainer
.mysql()
.map(a =>
MySqlConfig(
url = a.jdbcUrl,
username = a.username,
password = a.password
)
)
}

val hikariPoolConfigLayer: ZLayer[MySqlConfig, Nothing, HikariConnectionPoolConfig] =
ZLayer.fromFunction((conf: MySqlConfig) =>
HikariConnectionPoolConfig(url = conf.url, userName = conf.username, password = conf.password)
)
val poolLayer: ZLayer[HikariConnectionPoolConfig, Nothing, HikariConnectionPool] = HikariConnectionPool.live.orDie

override def spec: Spec[TestEnvironment, Any] =
specLayered.provideCustomShared(mySqlConfigLayer.orDie)

def specLayered: Spec[TestEnvironment with MySqlConfig, Any] =
suite("Hikaricp module")(
test("Pool size should be configurable") {
val poolSize = 20
(for {
cp <- ZIO.service[HikariConnectionPool]
} yield assertTrue(cp.dataSource.getMaximumPoolSize == poolSize))
.provideSomeLayer[TestEnvironment with MySqlConfig](
hikariPoolConfigLayer.map(_.update(_.copy(poolSize = poolSize))) >>> poolLayer
)
} @@ timeout(10.seconds) @@ withLiveClock,
test("Pool size should have 10 connections by default") {
(for {
cp <- ZIO.service[HikariConnectionPool]
_ <- ZIO.replicateZIO(10)(ZIO.scoped(cp.connection))
} yield assertTrue(cp.dataSource.getMaximumPoolSize == 10))
.provideSomeLayer[TestEnvironment with MySqlConfig](hikariPoolConfigLayer >>> poolLayer)
} @@ timeout(10.minutes) @@ withLiveClock,
test("It should be possible to acquire connections from the pool") {
val poolSize = 20
(for {
cp <- ZIO.service[HikariConnectionPool]
_ <-
ZIO.collectAllParDiscard(ZIO.replicate(poolSize)(ZIO.scoped(cp.connection *> ZIO.sleep(500.millisecond))))
} yield assert("")(Assertion.anything)).provideSomeLayer[TestEnvironment with MySqlConfig](
hikariPoolConfigLayer.map(_.update(_.copy(poolSize = poolSize))) >>> poolLayer
)
} @@ timeout(10.seconds) @@ withLiveClock,
test("Auto commit should be configurable") {
val autoCommit = false
(for {
cp <- ZIO.service[HikariConnectionPool]
} yield assertTrue(cp.dataSource.isAutoCommit == autoCommit))
.provideSomeLayer[TestEnvironment with MySqlConfig](
hikariPoolConfigLayer.map(_.update(_.copy(autoCommit = autoCommit))) >>> poolLayer
)
} @@ timeout(10.seconds) @@ withLiveClock,
test("Auto commit should be true by default") {
(for {
cp <- ZIO.service[HikariConnectionPool]
} yield assertTrue(cp.dataSource.isAutoCommit))
.provideSomeLayer[TestEnvironment with MySqlConfig](hikariPoolConfigLayer >>> poolLayer)
} @@ timeout(10.seconds) @@ withLiveClock,
test("Connection timeout should be configurable") {
val connectionTimeout = 2000L
(for {
cp <- ZIO.service[HikariConnectionPool]
} yield assertTrue(cp.dataSource.getConnectionTimeout == connectionTimeout))
.provideSomeLayer[TestEnvironment with MySqlConfig](
hikariPoolConfigLayer.map(_.update(_.copy(connectionTimeout = Some(connectionTimeout)))) >>> poolLayer
)
} @@ timeout(10.seconds) @@ withLiveClock,
test("Idle timeout should be configurable") {
val idleTimeout = 2000L
(for {
cp <- ZIO.service[HikariConnectionPool]
} yield assertTrue(cp.dataSource.getIdleTimeout == idleTimeout))
.provideSomeLayer[TestEnvironment with MySqlConfig](
hikariPoolConfigLayer.map(_.update(_.copy(idleTimeout = Some(idleTimeout)))) >>> poolLayer
)
} @@ timeout(10.seconds) @@ withLiveClock,
test("initialization fail timeout should be configurable") {
val initializationFailTimeout = 2000L
(for {
cp <- ZIO.service[HikariConnectionPool]
} yield assertTrue(cp.dataSource.getInitializationFailTimeout == initializationFailTimeout))
.provideSomeLayer[TestEnvironment with MySqlConfig](
hikariPoolConfigLayer.map(
_.update(_.copy(initializationFailTimeout = Some(initializationFailTimeout)))
) >>> poolLayer
)
} @@ timeout(10.seconds) @@ withLiveClock,
test("max lifetime should be configurable") {
val maxLifetime = 40000L
(for {
cp <- ZIO.service[HikariConnectionPool]
} yield assertTrue(cp.dataSource.getMaxLifetime == maxLifetime))
.provideSomeLayer[TestEnvironment with MySqlConfig](
hikariPoolConfigLayer.map(_.update(_.copy(maxLifetime = Some(maxLifetime)))) >>> poolLayer
)
} @@ timeout(10.seconds) @@ withLiveClock,
test("minimum idle should be configurable") {
val minimumIdle = 2
(for {
cp <- ZIO.service[HikariConnectionPool]
} yield assertTrue(cp.dataSource.getMinimumIdle == minimumIdle))
.provideSomeLayer[TestEnvironment with MySqlConfig](
hikariPoolConfigLayer.map(_.update(_.copy(minimumIdle = Some(minimumIdle)))) >>> poolLayer
)
} @@ timeout(10.seconds) @@ withLiveClock,
test("connection init SQL should be configurable") {
val initialSql = "SELECT 1 FROM test.test"
(for {
cp <- ZIO.service[HikariConnectionPool]
} yield assertTrue(cp.dataSource.getConnectionInitSql == initialSql))
.provideSomeLayer[TestEnvironment with MySqlConfig](
hikariPoolConfigLayer.map(_.update(_.copy(connectionInitSql = Some(initialSql)))) >>> poolLayer
)
} @@ timeout(10.seconds) @@ withLiveClock
) @@ sequential
}
23 changes: 23 additions & 0 deletions jdbc-hikaricp/src/test/scala/zio/sql/MySqlTestContainer.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package zio.sql

import com.dimafeng.testcontainers.MySQLContainer
import org.testcontainers.utility.DockerImageName
import zio._

final case class MySqlConfig(username: String, password: String, url: String)
object MySqlTestContainer {

def mysql(imageName: String = "mysql"): ZIO[Scope, Throwable, MySQLContainer] =
ZIO.acquireRelease {
ZIO.attemptBlocking {
val c = new MySQLContainer(
mysqlImageVersion = Option(imageName).map(DockerImageName.parse)
).configure { a =>
a.withInitScript("test_schema.sql")
()
}
c.start()
c
}
}(container => ZIO.attemptBlocking(container.stop()).orDie)
}