I've got simple cats-effect app, which download site from the URL given as argument. During downloading app is supposed to display "loading bar" by writting dots (.
) to console. I implemented it by doing race of two IOs one for downloading another for displaying dots.
This is whole app on scastie.
The most important part is here:
def loader(): IO[Unit] = for {
_ <- console.putStr(".")
_ <- timer.sleep(Duration(50, MILLISECONDS)) *> loader()
} yield {}
def download(url: String): IO[String] = IO.delay(Source.fromURL(url)).map(_.mkString)
def run(args: List[String]): IO[Unit] = {
args.headOption match {
case Some(url) =>
for {
content <- IO.race(download(url), loader()).map(_.left.get)
_ <- console.putStrLn() *> console.putStrLn(s"Downloaded site from $url. Size of downloaded content is ${content.length}.")
} yield {}
case None => console.putStrLn("Pass url as argument.")
}
}
Everything works as I expected, when I run it, I get:
.............. Downloaded site from https://www.scala-lang.org. Size of downloaded content is 47738.
Only problem is that app never exits.
As far as I checked loader IO gets cancelled correctly. I can even add something like this:
urlLoader.run(args) *> console.putStrLn("???") *> IO(ExitCode.Success)
And ???
gets displayed.
Also when I remove race, then app exits correctly.
So my question how can I fix this and make my app exit at the end?
To follow up on my comment above: the problem is that your ScheduledExecutorService
has threads running that prevent the JVM from exiting, even though your timer's tasks have been cancelled. There are several ways you could resolve this:
IO(ses.shutdown())
before IO(ExitCode.Success)
.newScheduledThreadPool
with a thread factory that daemonizes its threads.timer: Timer
that you get for free inside IOApp
.The last of these is almost definitely the right choice—using the timer (and ContextShift
) provided by IOApp
will give you reasonable defaults for this and other behaviors.