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

OutOfMemoryError when IO.uncancelable is used in recursive function #4573

Closed
tomohavvk opened this issue Mar 14, 2024 · 1 comment
Closed

Comments

@tomohavvk
Copy link

Version of cats-effect: 3.5.4

While going through the tutorial Producer consumer with taking care of cancellation I noticed that the code provided in the example terminates with a java.lang.OutOfMemoryError. The timing of the error's occurrence may vary depending on the Xmx VM option, but the error still occurs nonetheless.
I'm trying to understand whether this is the expected behavior and whether I should avoid using IO.uncancelable in recursive code, or if it's a BUG?

If this is the expected behaviour, it seems strange that the Typelevel website showcases an example implementation of a producer and consumer that eventually leads to an out-of-memory error due to memory overflow.
IMHO at least the behaviour should be mentioned in the tutorial.

Below is an example code that includes two programs. badProgram leads to an java.lang.OutOfMemoryError when using IO.uncancelable in recursive function, while the goodProgram works indefinitely without a loss in performance until manually terminated.

Code example - https://scastie.scala-lang.org/bjx9e0WsRq20XTxUprnCsg

import cats.effect._
import java.time.{LocalDateTime, ZoneOffset}

object OOMBug extends IOApp {

  override def run(args: List[String]): IO[ExitCode] =
    for {
      //      _ <- goodProgram(0)
      _ <- badProgram(0)
    } yield ExitCode.Success

  private val badProgram: Long => IO[Long] = counter =>
    IO.uncancelable { poll =>
      printIfNeed(counter) >> poll(badProgram(counter + 1)).onCancel(IO.unit)
    }

  private val goodProgram: Long => IO[Long] = counter =>
    pollImitation { poll =>
      printIfNeed(counter) >> poll(goodProgram(counter + 1))
    }

  private val printIfNeed: Long => IO[Unit] = counter =>
    IO.whenA(counter % 100000 == 0) {
      IO.delay(LocalDateTime.now.toInstant(ZoneOffset.UTC).toEpochMilli)
        .flatMap { millis =>
          IO.println(s"Time: $millis. Counter: $counter")
        }
    }

  private def pollImitation[A](body: Poll[IO] => IO[A]): IO[A] =
    body {
      new Poll[IO] {
        def apply[B](ioa: IO[B]): IO[B] = ioa
      }
    }
}

Error output:

Time: 1710428772216. Counter: 33400000
Time: 1710428772267. Counter: 33500000
2024-03-14T15:06:13.650Z [WARNING] Your app's responsiveness to a new asynchronous event (such as a new connection, an upstream response, or a timer) was in excess of 100 milliseconds. Your CPU is probably starving. Consider increasing the granularity of your delays or adding more cedes. This may also be a sign that you are unintentionally running blocking I/O operations (such as File or InetAddress) without the blocking combinator.
java.lang.OutOfMemoryError: Java heap space
	at cats.effect.ByteStack$.growIfNeeded(ByteStack.scala:48)
	at cats.effect.ByteStack$.push(ByteStack.scala:56)
	at cats.effect.IOFiber.runLoop(IOFiber.scala:582)
	at cats.effect.IOFiber.autoCedeR(IOFiber.scala:1423)
	at cats.effect.IOFiber.run(IOFiber.scala:119)
	at cats.effect.unsafe.WorkerThread.run(WorkerThread.scala:743)
@tomohavvk
Copy link
Author

Closed, since duplicate of typelevel/cats-effect#4043

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant