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

fix: switch to vertx server backend #223

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
22 changes: 14 additions & 8 deletions backend/src/main/scala/com/softwaremill/adopttapir/Main.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.softwaremill.adopttapir

import cats.effect.IO
import cats.effect.std.Dispatcher
import cats.effect.unsafe.implicits.global
import com.softwaremill.adopttapir.config.Config
import com.softwaremill.adopttapir.metrics.Metrics
Expand All @@ -14,14 +15,19 @@ object Main extends StrictLogging {
val config = Config.read
Config.log(config)

Dependencies
.wire(config)
.use { case Dependencies(httpApi) =>
/*
- allocates the http api resource, and never releases it (so that the http server is available
as long as our application runs)
*/
httpApi.resource.use(_ => IO.never)
Dispatcher[IO]
.use { dispatcher =>
{
Dependencies
.wire(config)
.use { case Dependencies(httpApi) =>
/*
allocates the http api resource, and never releases it (so that the http server is available
as long as our application runs)
*/
httpApi.resource(dispatcher).useForever
}
}
}
.unsafeRunSync()
Comment on lines +18 to 32
Copy link
Contributor Author

@geminicaprograms geminicaprograms Oct 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Current approach is based on Vert.x example
Alternatively for comprehension can be used but it is hard to tell if it is more readable ;)

  (for {
      dispatcher <- Dispatcher[IO]
      servers <- Dependencies
        .wire(config)
        .map { case Dependencies(httpApi) =>
          /*
        allocates the http api resource, and never releases it (so that the http server is available
        as long as our application runs)
           */
          httpApi.resource(dispatcher).useForever.unsafeRunSync()
        }
    } yield (dispatcher, servers)).useForever.unsafeRunSync()

@adamw WDYT?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can make it more readable :)

for {
  dispatcher <- Dispatcher[IO]
  httpApi <- Dependencies.wire(config)
  servers <- httpApi.resource(dispatcher)
  _ <- Resource.never
} yield whatever

}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
package com.softwaremill.adopttapir.http

import cats.effect.std.Dispatcher
import cats.effect.{IO, Resource}
import com.softwaremill.adopttapir.infrastructure.CorrelationIdInterceptor
import com.softwaremill.adopttapir.logging.FLogger
import com.softwaremill.adopttapir.util.ServerEndpoints
import com.typesafe.scalalogging.StrictLogging
import org.http4s.HttpRoutes
import org.http4s.blaze.server.BlazeServerBuilder
import io.vertx.core.Vertx
import io.vertx.core.http.HttpServer
import io.vertx.ext.web.Router
import sttp.capabilities.fs2.Fs2Streams
import sttp.model.Method
import sttp.tapir._
import sttp.tapir.server.ServerEndpoint
import sttp.tapir.server.http4s.{Http4sServerInterpreter, Http4sServerOptions}
import sttp.tapir.server.interceptor.cors.CORSInterceptor
import sttp.tapir.server.interceptor.cors.CORSConfig.AllowedMethods
import sttp.tapir.server.interceptor.cors.{CORSConfig, CORSInterceptor}
import sttp.tapir.server.metrics.prometheus.PrometheusMetrics
import sttp.tapir.server.model.ValuedEndpointOutput
import sttp.tapir.server.vertx.cats.{VertxCatsServerInterpreter, VertxCatsServerOptions}
import sttp.tapir.server.vertx.cats.VertxCatsServerInterpreter._
import sttp.tapir.static.ResourcesOptions
import sttp.tapir.swagger.SwaggerUIOptions
import sttp.tapir.swagger.bundle.SwaggerInterpreter
Expand All @@ -35,27 +39,16 @@ class HttpApi(
) extends StrictLogging {
private val apiContextPath = List("api", "v1")

private val serverOptions: Http4sServerOptions[IO] = Http4sServerOptions
.customiseInterceptors[IO]
.prependInterceptor(CorrelationIdInterceptor)
// all errors are formatted as json, and there are no other additional http4s routes
.defaultHandlers(msg => ValuedEndpointOutput(http.jsonErrorOutOutput, Error_OUT(msg)), notFoundWhenRejected = true)
.serverLog {
// using a context-aware logger for http logging
val flogger = new FLogger(logger)
Http4sServerOptions
.defaultServerLog[IO]
.doLogWhenHandled((msg, e) => e.fold(flogger.debug[IO](msg))(flogger.debug(msg, _)))
.doLogAllDecodeFailures((msg, e) => e.fold(flogger.debug[IO](msg))(flogger.debug(msg, _)))
.doLogExceptions((msg, e) => flogger.error[IO](msg, e))
.doLogWhenReceived(msg => flogger.debug[IO](msg))
}
.corsInterceptor(CORSInterceptor.default[IO])
.metricsInterceptor(prometheusMetrics.metricsInterceptor())
.options

private lazy val publicRoutes: HttpRoutes[IO] = Http4sServerInterpreter(serverOptions).toRoutes(allPublicEndpoints)
private lazy val adminRoutes: HttpRoutes[IO] = Http4sServerInterpreter(serverOptions).toRoutes(allAdminEndpoints)
private def serverOptions(dispatcher: Dispatcher[IO]): VertxCatsServerOptions[IO] =
VertxCatsServerOptions
.customiseInterceptors[IO](dispatcher)
.prependInterceptor(CorrelationIdInterceptor)
// all errors are formatted as json, and there are no other additional http4s routes
.defaultHandlers(msg => ValuedEndpointOutput(http.jsonErrorOutOutput, Error_OUT(msg)), notFoundWhenRejected = true)
// TODO customise the serverLog when available
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed in latest tapir

.corsInterceptor(CORSInterceptor.default[IO])
.metricsInterceptor(prometheusMetrics.metricsInterceptor())
.options

lazy val allPublicEndpoints: List[ServerEndpoint[Any with Fs2Streams[IO], IO]] = {
// creating the documentation using `mainEndpoints` without the /api/v1 context path; instead, a server will be added
Expand Down Expand Up @@ -84,16 +77,26 @@ class HttpApi(
(adminEndpoints ++ List(prometheusMetrics.metricsEndpoint)).toList

/** The resource describing the HTTP server; binds when the resource is allocated. */
lazy val resource: Resource[IO, (org.http4s.server.Server, org.http4s.server.Server)] = {
def resource(routes: HttpRoutes[IO], port: Int) =
BlazeServerBuilder[IO]
.bindHttp(port, config.host)
.withHttpApp(routes.orNotFound)
.resource
def resource(dispatcher: Dispatcher[IO]): Resource[IO, (HttpServer, HttpServer)] = {
def resource(dispatcher: Dispatcher[IO], endpoints: List[ServerEndpoint[Any with Fs2Streams[IO], IO]], port: Int) = {
Resource.make(
IO.delay {
val vertx = Vertx.vertx()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should crate a single shared Vertx

val server = vertx.createHttpServer()
val router = Router.router(vertx)
endpoints
.map(endpoint => VertxCatsServerInterpreter[IO](serverOptions(dispatcher)).route(endpoint))
.foreach(attach => attach(router))
server.requestHandler(router).listen(port, config.host)
}.flatMap(_.asF[IO])
)({ server =>
IO.delay(server.close).flatMap(_.asF[IO].void)
})
}

for {
public <- resource(publicRoutes, config.port)
admin <- resource(adminRoutes, config.adminPort)
public <- resource(dispatcher, allPublicEndpoints, config.port)
admin <- resource(dispatcher, allAdminEndpoints, config.adminPort)
} yield (public, admin)
}
}
4 changes: 1 addition & 3 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,9 @@ val logbackClassicVersion = "1.4.4"
val scalaTestVersion = "3.2.14"

val httpDependencies = Seq(
"org.http4s" %% "http4s-blaze-server" % http4sBlazeServerVersion,
"org.http4s" %% "http4s-circe" % http4sCirceVersion,
"com.softwaremill.sttp.client3" %% "async-http-client-backend-fs2" % sttpVersion,
"com.softwaremill.sttp.client3" %% "slf4j-backend" % sttpVersion,
"com.softwaremill.sttp.tapir" %% "tapir-http4s-server" % tapirVersion,
"com.softwaremill.sttp.tapir" %% "tapir-vertx-server-cats" % tapirVersion,
"com.softwaremill.sttp.tapir" %% "tapir-sttp-stub-server" % tapirVersion
)

Expand Down