Search code examples
scalaloggingconcurrencyslf4jscala-3

What is the reason for IO application not closing its execution?


Why does this hang?

import cats.effect.IO
import cats.effect.unsafe.implicits.global
import com.typesafe.config.ConfigFactory
import io.circe.config.parser
import org.typelevel.log4cats._
import org.typelevel.log4cats.slf4j._

object ItsElenApp extends App:

  private val logger: SelfAwareStructuredLogger[IO] = LoggerFactory[IO].getLogger

  val ops = for
    _ <- logger.info("aa")
    _ <- IO(println("bcc"))
    _ <- logger.error("bb")
  yield ()

  ops.unsafeRunSync()

  println("end")

It prints:

INFO 2022-06-19 11:56:25,303 ItsElenApp - aa
bcc

and keeps running. Is it the log4cats library, or am I using the App object in a wrong way. Or do I have to close an execution context?


Solution

  • The recommended way of running cats.effect.IO-based apps is using cats.effect.IOApp (or cats.effect.ResourceApp):

    object MyApp extends IOApp:
    
      // notice it takes List[String] rather than Array[String]
      def run(args: List[String]): IO[ExitCode] = ...
    

    this would run the application, handle setting up exit code, etc. It closes the app when run reaches the end, which might be necessary if the default thread pool is non-daemon. If you don't want to use IOApp you might need to close JVM manually, also taking into consideration that exception might have been thrown on .unsafeRunSync().

    Extending App is on the other hand not recommended in general. Why? It uses special Delayed mechanism where the whole body of a class (its constructor) is lazy. This makes it harder to reason about the initialization, which is why this mechanism became deprecated/discouraged. If you are not using IOApp it is better to implement things like:

    object MyProgram:
    
      // Java requires this signature
      def main(args: Array[String]): Unit = ...
    

    which in your case could look like

    object ItsElenApp:
    
      private val logger: SelfAwareStructuredLogger[IO] = LoggerFactory[IO].getLogger
    
      def main(args: Array[String]): Unit =
        val ops = for
          _ <- logger.info("aa")
          _ <- IO(println("bcc"))
          _ <- logger.error("bb")
        yield ()
    
        // IOApp could spare you this
        try
          ops.unsafeRunSync()
        finally
          println("end")
          sys.exit(0)