How to gracefully shutdown http4s
Asked Answered
V

1

7

I'm using http4s BlazeServer 0.21, how can I graceful shutdown? I want to reject all upcoming requests, and keep process unfinished requests and response back, within a hard shutdown time.

I tried starting server with serveWhile and set a shutdownHook SignallingRef. The server stream & middleware defer as expected (so our metrics & log middleware still log this response)

//serverStream
for {
  signal   <- fs2.Stream.eval(SignallingRef[F, Boolean](false))
  exitCode <- fs2.Stream.eval(Ref[F].of(ExitCode.Success))
  _        <- fs2.Stream.eval(shutdown(signal))
  server <- BlazeServerBuilder[F]
    .bindHttp(8080, "0.0.0.0")
    .withHttpApp(httpApp)
    .serveWhile(signal, exitCode)
} yield server
def shutdown[F[_]: Effect](interrupter: SignallingRef[F, Boolean]): F[Unit] = {
  LiftIO[F].liftIO(IO {
    sys.addShutdownHook {
      ...
      interrupter.set(true)
    }
  })
}
object Server extends IOApp {
  def run(args: List[String]): IO[ExitCode] =
    serverStream[IO].compile.drain.as(ExitCode.Success)
}

but the http server doesn't work as I expect, seems like http4s's internal ServerChannel has its own shutdownHook and cancel all the responses already.

any suggestion/workaround? or maybe just a way to hold and don't kill requests for x seconds is also appreciated.

Virgel answered 16/3, 2020 at 7:12 Comment(0)
T
5

The server is hooked for SIGTERM as a matter of convenience.

As a convenience, cats-effect provides an cats.effect.IOApp trait with an abstract run method that returns a IO[ExitCode]. An IOApp runs the process and adds a JVM shutdown hook to interrupt the infinite process and gracefully shut down your server when a SIGTERM is received.

And if you want to shutdown using an URL e.g. http://localhost:8080/ops/shutdown/true, it works too.

The code is pretty straight forward,

class SysOpsEndpoints[F[_]: Sync](signal: SignallingRef[F, Boolean]) extends Http4sDsl[F] {
  private def shutdown: HttpRoutes[F] =
    HttpRoutes.of[F] {
      case GET -> Root / "shutdown" / shutdown =>
        for {
          _ <- signal.set(Try(shutdown.toBoolean).getOrElse(false))
          result <- Ok(s"Shutdown: $shutdown")
        } yield result
    }
}

object SysOpsEndpoints {
  def endpoints[F[_]: Sync](signal: SignallingRef[F, Boolean]): HttpRoutes[F] =
    new SysOpsEndpoints(signal).shutdown
}

and the setup for the server for-comprehension is similar to yours,

for {
  signal   <- fs2.Stream.eval(SignallingRef[F, Boolean](false))
  exitCode <- fs2.Stream.eval(Ref[F].of(ExitCode.Success))
  httpApp = Router(
        "/ops" -> SysOpsEndpoints.endpoints(signal)
      ).orNotFound
  server <- BlazeServerBuilder[F](serverEc)
    .bindHttp(8080, "0.0.0.0")
    .withHttpApp(httpApp)
    .serveWhile(signal, exitCode)
} yield server

Thickleaf answered 25/5, 2020 at 15:14 Comment(3)
Nice answer. Just to note, this will kill all open requests to the server once the signal is set.Jorgan
How is this graceful if this kills all open requests?Woodyard
@AmitKumar what I mention here “gracefully shutdown” refers to closing all opened connections. I haven’t thought about open requests. Do you have any idea to resolve that?Thickleaf

© 2022 - 2024 — McMap. All rights reserved.