Home > front end >  What is the reason for IO application not closing its execution?
What is the reason for IO application not closing its execution?

Time:06-19

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?

CodePudding user response:

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)

CodePudding user response:

Because log4cats' logging functions seem to be asynchronous. Meaning they might run on a different thread, not in sequence with the other operations.

That means the main thread might finish but the other threads keep being opened and never closing.

If I use ops.unsafeRunTimed(2.seconds) everything executes and closes. But the latter log lines come only at the end of the time. Looks like the logging is somehow lazy and only finishes if it is asked to. I'm not sure.

They write you shouldn't use unsafeRunTimed in production code.

Of course if you use IOApp, everything executes normally again.

But how would you write this in production code, cleanly, nicely without an IOApp? I think there should be a clean way of telling log4cats to finish the operations, return their async fibers results and close everything. All manually.

  • Related