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

IOApp hangs when Resource contains exception #487

Closed
LMnet opened this issue Feb 12, 2019 · 6 comments

Comments

5 participants
@LMnet
Copy link

commented Feb 12, 2019

It looks like IOApp didn't handle all error cases.

In the following example, I expect that the application will finish with the error code and print stack trace in the console:

import cats.effect.{ExitCode, IO, IOApp, Resource}

object ResourceIoAppTest extends IOApp {
  override def run(args: List[String]): IO[ExitCode] = {
    val r = Resource.liftF(IO.delay(throw new RuntimeException("Test")))
    r.use { _ =>
      IO.delay(ExitCode.Success)
    }
  }
}

But actual behavior differs: application prints stack trace and hangs. It could be killed only with kill -9.

When I run this app with the sbt run I see this:

java.lang.RuntimeException: Test
        at cats.effect.internals.ResourceIoAppTest$.$anonfun$run$1(Foo.scala:23)
        at cats.effect.internals.IORunLoop$.cats$effect$internals$IORunLoop$$loop(IORunLoop.scala:87)
        at cats.effect.internals.IORunLoop$.start(IORunLoop.scala:34)
        at cats.effect.internals.IOBracket$.$anonfun$apply$1(IOBracket.scala:44)
        at cats.effect.internals.IOBracket$.$anonfun$apply$1$adapted(IOBracket.scala:34)
        at cats.effect.internals.IORunLoop$RestartCallback.start(IORunLoop.scala:337)
        at cats.effect.internals.IORunLoop$.cats$effect$internals$IORunLoop$$loop(IORunLoop.scala:119)
        at cats.effect.internals.IORunLoop$RestartCallback.signal(IORunLoop.scala:351)
        at cats.effect.internals.IORunLoop$RestartCallback.apply(IORunLoop.scala:372)
        at cats.effect.internals.IORunLoop$RestartCallback.apply(IORunLoop.scala:312)
        at cats.effect.internals.IOShift$Tick.run(IOShift.scala:36)
        at java.util.concurrent.ForkJoinTask$RunnableExecuteAction.exec(ForkJoinTask.java:1402)
        at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:289)
        at java.util.concurrent.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1056)
        at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1692)
        at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:157)

Exception: sbt.TrapExitSecurityException thrown from the UncaughtExceptionHandler in thread "run-main-0"

I tried to investigate this issue and I found, that in IOAppPlatform.mainFiber even after redeem call the io could fail. I changed io.start(contextShift.value).flatMap to io.start(contextShift.value).bracket. It helps a little: application could now be killed with the simple kill command. But the app still hangs and don't finish properly.

@oleg-py

This comment has been minimized.

Copy link
Contributor

commented Feb 12, 2019

Seems to be hanging on shutdown hook added in IOAppPlatform if bracket acquisition ever fails, unrelated to Resource. For example, this implementation hangs indefinitely:

  override def run(args: List[String]): IO[ExitCode] = {
    IO.delay(throw new RuntimeException("Test"))
      .bracket(_ => IO.unit)(_ => IO.unit)
      .attempt.map(_ => ExitCode.Success)
  }

The structure of fiber.cancel in that hook seems suspicious. It's a cancel token that refers to itself, creating a loop, but it doesn't seem that it's that code that causes a loop.

@LMnet

This comment has been minimized.

Copy link
Author

commented Feb 13, 2019

@oleg-py Without fiber.cancel.unsafeRunSync() in the shutdown hook:

  1. My example works perfectly fine.
  2. Your example with bracket finished normally, but the code in the release section don't execute at all (I added some logging on each step).

And I don't understand why you said that fiber.cancel in the shutdown hook refers to itself. Could you give some more details about this?

@oleg-py

This comment has been minimized.

Copy link
Contributor

commented Feb 13, 2019

@LMnet

code in the release section don't execute at all

Yes, this is expected behavior: if you didn't acquire a resource, there is nothing to release.

why you said that fiber.cancel in the shutdown hook refers to itself

I just took a quick look in a debugger. Posted this in case somebody more familiar (*cough* @alexandru *cough*) wants to take a look too.

@cquiroz

This comment has been minimized.

Copy link

commented Mar 18, 2019

I'm also observing this and I can reproduce it with this gist

https://gist.github.com/cquiroz/d989bb3320c0ed0a644fbd8b0c327c01

@barambani

This comment has been minimized.

Copy link
Contributor

commented Mar 19, 2019

I'm facing the same issue. My use case can be reduced to the following

import cats.effect.{ExitCode, IO, IOApp, Resource}
import log.effect.fs2.SyncLogWriter.consoleLog
import cats.syntax.flatMap._

object Main extends IOApp {

  def run(args: List[String]): IO[ExitCode] =
    Resource.make(acquireError)(release).use(use)
      .redeemWith(
        _ => IO.pure(ExitCode.Error),
        _ => IO.pure(ExitCode.Success)
      )

  def acquireError: IO[Int] =
    consoleLog[IO].info("Acquiring, with error") >> IO.delay(throw new Exception("fail"))

  def release(i: Int): IO[Unit] =
    consoleLog[IO].info("Releasing, no error") >> IO.unit

  def use(i: Int): IO[Int] =
    consoleLog[IO].info("Using, no error") >> IO.delay(i + 1)
}

that hangs with

[info] Running (fork) Main
[info] [info] - [scala-execution-context-global-10] Acquiring, with error

As I need a workaround I was thinking to something like

import cats.effect.{ExitCode, IO, IOApp, Resource}
import log.effect.fs2.SyncLogWriter.consoleLog
import cats.syntax.flatMap._

object Main extends IOApp {

  def run(args: List[String]): IO[ExitCode] =
    Resource.make(acquireError.attempt)(_.fold(_ => IO.unit, release))
      .use(_.fold(IO.raiseError, use))
      .redeemWith(
        _ => IO.pure(ExitCode.Error),
        _ => IO.pure(ExitCode.Success)
      )

  def acquireError: IO[Int] =
    consoleLog[IO].info("Acquiring, with error") >> IO.delay(throw new Exception("fail"))

  def release(i: Int): IO[Unit] =
    consoleLog[IO].info("Releasing, no error") >> IO.unit

  def use(i: Int): IO[Int] =
    consoleLog[IO].info("Using, no error") >> IO.delay(i + 1)
}

That actually exits as expected.

[info] Running (fork) Main
[info] [info] - [scala-execution-context-global-10] Acquiring, with error
[error] Nonzero exit code returned from runner: 1
[error] (Compile / run) Nonzero exit code returned from runner: 1
[error] Total time: 6 s, completed 19-Mar-2019 00:56:01
@RafalSumislawski

This comment has been minimized.

Copy link
Contributor

commented Mar 23, 2019

I've run into this issue when dealing with http4s timeouts. IMO the issue is caused by not calling deferredRelease.complete here:

cb(error.asInstanceOf[Either[Throwable, B]])
. As a result if acquisition of a resource fails, a cancel will never end.

RafalSumislawski added a commit to RafalSumislawski/cats-effect that referenced this issue Apr 9, 2019

rossabaker added a commit that referenced this issue Apr 10, 2019

Merge pull request #499 from RafalSumislawski/fix-bracket-cancellation
Fix infinitely waiting cancellation of a bracket which failed to acquire. Fixes #487

RafalSumislawski added a commit to RafalSumislawski/http4s that referenced this issue May 3, 2019

- use cats-effect 1.3.0
- remove the workarounds for typelevel/cats-effect#487 introduced in http4s#2470 as the issue is fix in cats-effects 1.3.0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.