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
Closed

IOApp hangs when Resource contains exception #487

LMnet opened this issue Feb 12, 2019 · 6 comments

Comments

@LMnet
Copy link

@LMnet LMnet 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
Copy link
Contributor

@oleg-py oleg-py 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
Copy link
Author

@LMnet LMnet 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
Copy link
Contributor

@oleg-py oleg-py 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
Copy link

@cquiroz cquiroz commented Mar 18, 2019

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

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

@barambani
Copy link

@barambani barambani 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
Copy link
Contributor

@RafalSumislawski RafalSumislawski 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
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
- 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
Labels
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
5 participants