Skip to content

Commit

Permalink
perf(db): use HikariCP instead of JDBC DriverManager for manage c…
Browse files Browse the repository at this point in the history
…onnection
  • Loading branch information
yoshinorin committed Jul 20, 2024
1 parent 2cf2b39 commit 6bf38aa
Show file tree
Hide file tree
Showing 26 changed files with 157 additions and 148 deletions.
1 change: 1 addition & 0 deletions project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ object Dependencies {
"com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % jsoniterVersion % "test-internal",
"org.mariadb.jdbc" % "mariadb-java-client" % "3.4.0",
"org.tpolecat" %% "doobie-core" % doobieVersion,
"org.tpolecat" %% "doobie-hikari" % doobieVersion,
// "org.tpolecat" %% "doobie-scalatest" % doobieVersion % Test,
"org.wvlet.airframe" %% "airframe-ulid" % "24.7.0",
"com.github.ben-manes.caffeine" % "caffeine" % "3.1.8",
Expand Down
28 changes: 16 additions & 12 deletions src/main/scala/net/yoshinorin/qualtet/BootStrap.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import cats.effect.{ExitCode, IO, IOApp}
import cats.effect.kernel.Resource
import org.typelevel.log4cats.SelfAwareStructuredLogger
import org.typelevel.log4cats.{LoggerFactory => Log4CatsLoggerFactory}
import org.typelevel.log4cats.slf4j.{Slf4jFactory => Log4CatsSlf4jFactory}
import org.http4s.*
import org.http4s.ember.server.EmberServerBuilder
import org.http4s.server.Server
Expand All @@ -14,7 +15,7 @@ import scala.concurrent.duration._

object BootStrap extends IOApp {

import net.yoshinorin.qualtet.Modules.log4catsLogger
given log4catsLogger: Log4CatsLoggerFactory[IO] = Log4CatsSlf4jFactory.create[IO]

val logger: SelfAwareStructuredLogger[IO] = Log4CatsLoggerFactory[IO].getLoggerFromClass(this.getClass)

Expand All @@ -33,17 +34,20 @@ object BootStrap extends IOApp {
val host = Ipv4Address.fromString(Modules.config.http.host).getOrElse(ipv4"127.0.0.1")
val port = Port.fromInt(Modules.config.http.port).getOrElse(port"9001")

(for {
_ <- logger.info(ApplicationInfo.asJson)
_ <- IO(Modules.migrator.migrate(Modules.contentTypeService))
routes <- Modules.router.withCors.map[Kleisli[IO, Request[IO], Response[IO]]](x => x.orNotFound)
httpApp <- IO(new HttpAppBuilder(routes).build)
server <- IO(
server(host, port, httpApp)
.use(_ => IO.never)
.as(ExitCode.Success)
)
} yield server).flatMap(identity)
Modules.transactorResource.use { tx =>
val modules = new Modules(tx)
(for {
_ <- logger.info(ApplicationInfo.asJson)
_ <- IO(modules.migrator.migrate(modules.contentTypeService))
routes <- modules.router.withCors.map[Kleisli[IO, Request[IO], Response[IO]]](x => x.orNotFound)
httpApp <- IO(new HttpAppBuilder(routes).build)
server <- IO(
server(host, port, httpApp)
.use(_ => IO.never)
.as(ExitCode.Success)
)
} yield server).flatMap(identity)
}

}
}
43 changes: 24 additions & 19 deletions src/main/scala/net/yoshinorin/qualtet/Modules.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package net.yoshinorin.qualtet

import cats.effect.IO
import doobie.ConnectionIO
import doobie.util.transactor.Transactor
import doobie.util.transactor.Transactor.Aux
import org.typelevel.log4cats.{LoggerFactory => Log4CatsLoggerFactory}
import org.typelevel.log4cats.slf4j.{Slf4jFactory => Log4CatsSlf4jFactory}
Expand Down Expand Up @@ -45,21 +46,25 @@ import net.yoshinorin.qualtet.http.routes.v1.{
TagRoute => TagRouteV1
}
import net.yoshinorin.qualtet.infrastructure.db.Migrator
import net.yoshinorin.qualtet.infrastructure.db.doobie.DoobieExecuter
import net.yoshinorin.qualtet.infrastructure.db.doobie.{DoobieExecuter, DoobieTransactor}

import pdi.jwt.JwtAlgorithm
import java.security.SecureRandom
import java.util.concurrent.TimeUnit
import doobie.util.transactor.Transactor
import net.yoshinorin.qualtet.infrastructure.db.doobie.DoobieTransactor

object Modules {

val config = ApplicationConfig.load
val doobieTransactor: DoobieTransactor[Aux] = summon[DoobieTransactor[Aux]]
val transactorResource = doobieTransactor.make(config.db)
}

class Modules(tx: Transactor[IO]) {

val config = ApplicationConfig.load

given log4catsLogger: Log4CatsLoggerFactory[IO] = Log4CatsSlf4jFactory.create[IO]
given dbContext: DoobieExecuter = new DoobieExecuter(doobieTransactor.make(config.db))
given dbContext: DoobieExecuter = new DoobieExecuter(tx)

val migrator: Migrator = new Migrator(config.db)

// NOTE: for generate JWT. They are reset when re-boot application.
Expand Down Expand Up @@ -137,23 +142,23 @@ object Modules {
feedService
)

val authProvider = new AuthProvider(Modules.authService)
val authProvider = new AuthProvider(authService)
val corsProvider = new CorsProvider(Modules.config.cors)

val archiveRouteV1 = new ArchiveRouteV1(Modules.archiveService)
val articleRouteV1 = new ArticleRouteV1(Modules.articleService)
val authorRouteV1 = new AuthorRouteV1(Modules.authorService)
val authRouteV1 = new AuthRouteV1(Modules.authService)
val cacheRouteV1 = new CacheRouteV1(authProvider, Modules.cacheService)
val contentTypeRouteV1 = new ContentTypeRouteV1(Modules.contentTypeService)
val contentRouteV1 = new ContentRouteV1(authProvider, Modules.contentService)
val feedRouteV1 = new FeedRouteV1(Modules.feedService)
val archiveRouteV1 = new ArchiveRouteV1(archiveService)
val articleRouteV1 = new ArticleRouteV1(articleService)
val authorRouteV1 = new AuthorRouteV1(authorService)
val authRouteV1 = new AuthRouteV1(authService)
val cacheRouteV1 = new CacheRouteV1(authProvider, cacheService)
val contentTypeRouteV1 = new ContentTypeRouteV1(contentTypeService)
val contentRouteV1 = new ContentRouteV1(authProvider, contentService)
val feedRouteV1 = new FeedRouteV1(feedService)
val homeRoute: HomeRoute = new HomeRoute()
val searchRouteV1 = new SearchRouteV1(Modules.searchService)
val seriesRouteV1 = new SeriesRouteV1(authProvider, Modules.seriesService)
val sitemapRouteV1 = new SitemapRouteV1(Modules.sitemapService)
val systemRouteV1 = new SystemRouteV1(Modules.config.http.endpoints.system)
val tagRouteV1 = new TagRouteV1(authProvider, Modules.tagService, Modules.articleService)
val searchRouteV1 = new SearchRouteV1(searchService)
val seriesRouteV1 = new SeriesRouteV1(authProvider, seriesService)
val sitemapRouteV1 = new SitemapRouteV1(sitemapService)
val systemRouteV1 = new SystemRouteV1(config.http.endpoints.system)
val tagRouteV1 = new TagRouteV1(authProvider, tagService, articleService)

val router = new net.yoshinorin.qualtet.http.Router(
corsProvider,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
package net.yoshinorin.qualtet.infrastructure.db.doobie

import cats.effect.IO
import cats.effect.{IO, Resource}
import doobie.*
import doobie.util.transactor.Transactor.Aux
import doobie.hikari.*
import com.zaxxer.hikari.HikariConfig
import net.yoshinorin.qualtet.config.DBConfig

trait DoobieTransactor[F[G[_], _]] {
def make(config: DBConfig): Transactor[IO]
def make(config: DBConfig): Resource[IO, HikariTransactor[IO]]
}

object DoobieTransactor {

/*
given DoobieTransactor: DoobieTransactor[Aux] = {
new DoobieTransactor[Aux] {
override def make(config: DBConfig): Transactor[IO] = {
Expand All @@ -24,5 +27,24 @@ object DoobieTransactor {
}
}
}
*/

given DoobieTransactor: DoobieTransactor[Aux] = {
new DoobieTransactor[Aux] {
override def make(config: DBConfig): Resource[IO, HikariTransactor[IO]] = {
for {
hikariConfig <- Resource.pure {
val hConfig = new HikariConfig()
hConfig.setDriverClassName("org.mariadb.jdbc.Driver")
hConfig.setJdbcUrl(config.url.toString())
hConfig.setUsername(config.user.toString())
hConfig.setPassword(config.password.toString())
hConfig
}
xa <- HikariTransactor.fromHikariConfig[IO](hikariConfig)
} yield xa
}
}
}

}
55 changes: 24 additions & 31 deletions src/main/scala/net/yoshinorin/qualtet/tasks/CreateAuthor.scala
Original file line number Diff line number Diff line change
@@ -1,53 +1,46 @@
package net.yoshinorin.qualtet.tasks

import cats.effect.IO
import cats.implicits.catsSyntaxEq
import doobie.ConnectionIO
import doobie.util.transactor.Transactor.Aux
import cats.effect.{ExitCode, IO, IOApp}
import org.slf4j.LoggerFactory
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import net.yoshinorin.qualtet.domains.authors.{Author, AuthorDisplayName, AuthorName, AuthorRepository, AuthorService, BCryptPassword}
import net.yoshinorin.qualtet.infrastructure.db.doobie.DoobieExecuter
import net.yoshinorin.qualtet.Modules.*
import net.yoshinorin.qualtet.domains.authors.{Author, AuthorDisplayName, AuthorName, BCryptPassword}
import net.yoshinorin.qualtet.Modules
import net.yoshinorin.qualtet.syntax.*
import net.yoshinorin.qualtet.domains.authors.ResponseAuthor

import cats.effect.unsafe.implicits.global
import net.yoshinorin.qualtet.infrastructure.db.doobie.DoobieTransactor

object CreateAuthor {
object CreateAuthor extends IOApp {

private val logger = LoggerFactory.getLogger(this.getClass)

val doobieTransactor: DoobieTransactor[Aux] = summon[DoobieTransactor[Aux]]

given dbContext: DoobieExecuter = new DoobieExecuter(doobieTransactor.make(config.db))
val authorRepository: AuthorRepository[ConnectionIO] = summon[AuthorRepository[ConnectionIO]]
val authorService = new AuthorService(authorRepository)

def main(args: Array[String]): Unit = {
def run(args: List[String]): IO[ExitCode] = {
if (args.length =!= 3) {
throw new IllegalArgumentException("args must be three length.")
}

// https://docs.spring.io/spring-security/site/docs/current/reference/html5/#authentication-password-storage-bcrypt
val bcryptPasswordEncoder = new BCryptPasswordEncoder()
(for {
author <- authorService.create(
Author(name = AuthorName(args(0)), displayName = AuthorDisplayName(args(1)), password = BCryptPassword(bcryptPasswordEncoder.encode(args(2))))
)
} yield author)
.handleErrorWith { e =>
IO.pure(e)
}
.unsafeRunSync() match {
case a: ResponseAuthor =>
logger.info(s"author created: ${a.asJson}")
logger.info("shutting down...")
case e: Exception =>
logger.error(e.getMessage)
case _ =>
logger.error("unknown error")

Modules.transactorResource.use { tx =>
val modules = new Modules(tx)
(for {
author <- modules.authorService.create(
Author(name = AuthorName(args(0)), displayName = AuthorDisplayName(args(1)), password = BCryptPassword(bcryptPasswordEncoder.encode(args(2))))
)
_ <- IO(logger.info(s"author created: ${author.asJson}"))
_ <- IO(println(s"author created: ${author.asJson}"))
} yield author)
.handleErrorWith { e =>
e match
case e: Exception =>
IO(logger.error(e.getMessage))
case _ =>
IO(logger.error("unknown error"))
}
.unsafeRunSync() // FIXME
IO(ExitCode.Success)
}
}
}
10 changes: 3 additions & 7 deletions src/test/scala/net/yoshinorin/qualtet/auth/AuthServiceSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,23 @@ package net.yoshinorin.qualtet.auth
import cats.effect.IO
import net.yoshinorin.qualtet.domains.authors.ResponseAuthor
import net.yoshinorin.qualtet.domains.errors.{NotFound, Unauthorized}
import net.yoshinorin.qualtet.Modules.*
import net.yoshinorin.qualtet.Modules
import net.yoshinorin.qualtet.fixture.Fixture.*
import net.yoshinorin.qualtet.infrastructure.db.doobie.DoobieExecuter
import org.scalatest.wordspec.AnyWordSpec
import cats.effect.unsafe.implicits.global

// testOnly net.yoshinorin.qualtet.auth.AuthServiceSpec
class AuthServiceSpec extends AnyWordSpec {

val tx = doobieTransactor.make(Modules.config.db)
given dbContext: DoobieExecuter = new DoobieExecuter(tx)

val mod = Modules(fixtureTx)
val a: ResponseAuthor = authorService.findByName(author.name).unsafeRunSync().get
val a2: ResponseAuthor = authorService.findByName(author2.name).unsafeRunSync().get

"AuthService" should {

"generate token" in {
val token = authService.generateToken(RequestToken(a.id, "pass")).unsafeRunSync().token
assert(jwtInstance.decode[IO](token).unsafeRunSync().isRight)
val token = mod.authService.generateToken(RequestToken(a.id, "pass")).unsafeRunSync().token
assert(mod.jwtInstance.decode[IO](token).unsafeRunSync().isRight)
}

"find an author from JWT string" in {
Expand Down
15 changes: 9 additions & 6 deletions src/test/scala/net/yoshinorin/qualtet/auth/JwtSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ package net.yoshinorin.qualtet.auth
import cats.effect.IO
import net.yoshinorin.qualtet.domains.authors.{Author, AuthorDisplayName, AuthorId, AuthorName}
import net.yoshinorin.qualtet.domains.errors.Unauthorized
import net.yoshinorin.qualtet.Modules.*
import net.yoshinorin.qualtet.fixture.Fixture.validBCryptPassword
import net.yoshinorin.qualtet.fixture.Fixture.{fixtureTx, validBCryptPassword}
import net.yoshinorin.qualtet.Modules
import net.yoshinorin.qualtet.syntax.*
import net.yoshinorin.qualtet.validator.Validator

import org.scalatest.wordspec.AnyWordSpec
import pdi.jwt.exceptions.JwtValidationException
import wvlet.airframe.ulid.ULID
import net.yoshinorin.qualtet.syntax.*

import cats.effect.unsafe.implicits.global

Expand All @@ -18,6 +19,8 @@ import java.time.Instant
// testOnly net.yoshinorin.qualtet.auth.JwtSpec
class JwtSpec extends AnyWordSpec {

val mod = Modules(fixtureTx)
val config = Modules.config
val jc: JwtClaim = JwtClaim(
iss = config.jwt.iss,
aud = config.jwt.aud,
Expand All @@ -30,7 +33,7 @@ class JwtSpec extends AnyWordSpec {
"Jwt" should {
"encode and decode" in {
val id = ULID.newULIDString.toLower
val jwtString = jwtInstance.encode(
val jwtString = mod.jwtInstance.encode(
Author(
id = AuthorId(id),
name = AuthorName("Jhon"),
Expand All @@ -39,7 +42,7 @@ class JwtSpec extends AnyWordSpec {
)
)

jwtInstance.decode[IO](jwtString).unsafeRunSync() match {
mod.jwtInstance.decode[IO](jwtString).unsafeRunSync() match {
case Right(j) => {
assert(j.iss === config.jwt.iss)
assert(j.aud === config.jwt.aud)
Expand All @@ -55,7 +58,7 @@ class JwtSpec extends AnyWordSpec {
val ioInstance = implicitly[cats.Monad[IO]]

"throw exception caused by not signed JSON" in {
val maybeJwtClaims = jwtInstance.decode[IO](
val maybeJwtClaims = mod.jwtInstance.decode[IO](
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
)
assert(maybeJwtClaims.unsafeRunSync().left.getOrElse("").isInstanceOf[JwtValidationException])
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package net.yoshinorin.qualtet.cache

import net.yoshinorin.qualtet.Modules.*
import net.yoshinorin.qualtet.fixture.Fixture.*
import org.scalatest.wordspec.AnyWordSpec

import cats.effect.unsafe.implicits.global
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package net.yoshinorin.qualtet.domains.archives

import net.yoshinorin.qualtet.Modules.*
import net.yoshinorin.qualtet.fixture.Fixture.*
import org.scalatest.wordspec.AnyWordSpec
import org.scalatest.BeforeAndAfterAll
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package net.yoshinorin.qualtet.domains.articles
import net.yoshinorin.qualtet.domains.contents.{Path, RequestContent}
import net.yoshinorin.qualtet.domains.robots.Attributes
import net.yoshinorin.qualtet.domains.tags.TagName
import net.yoshinorin.qualtet.Modules.*
import net.yoshinorin.qualtet.fixture.Fixture.*
import net.yoshinorin.qualtet.http.ArticlesQueryParameter
import org.scalatest.wordspec.AnyWordSpec
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@ import net.yoshinorin.qualtet.domains.contents.{Path, RequestContent}
import net.yoshinorin.qualtet.domains.robots.Attributes
import net.yoshinorin.qualtet.domains.series.*
import net.yoshinorin.qualtet.fixture.Fixture.*
import net.yoshinorin.qualtet.infrastructure.db.doobie.DoobieExecuter
import net.yoshinorin.qualtet.Modules
import net.yoshinorin.qualtet.Modules.*
import org.scalatest.wordspec.AnyWordSpec
import org.scalatest.BeforeAndAfterAll

Expand All @@ -16,9 +13,6 @@ import org.scalatest.Ignore
@Ignore // TODO: write testcode when implement delete feature
class ContentSerializingServiceSpec extends AnyWordSpec with BeforeAndAfterAll {

val tx = doobieTransactor.make(Modules.config.db)
given dbContext: DoobieExecuter = new DoobieExecuter(tx)

val requestSeries: RequestSeries = RequestSeries(
title = "Content Serializing Service Spec",
name = SeriesName("content-serializing-service-spec"),
Expand Down
Loading

0 comments on commit 6bf38aa

Please sign in to comment.