3

If I run this program from SBT shell, then cancel it, it will keep printing "hello". I have to exit SBT to make it stop. Why is that?

import cats.effect.{ExitCode, IO, IOApp}
import fs2.Stream
import scala.concurrent.duration._

object FS2 extends IOApp {

  override def run(args: List[String]) = 
      Stream.awakeEvery[IO](5.seconds).map { _ =>
        println("hello")
      }.compile.drain.as(ExitCode.Error)
}
Krzysztof Atłasik
  • 21,985
  • 6
  • 54
  • 76
Cpt. Senkfuss
  • 1,347
  • 3
  • 12
  • 20

1 Answers1

2

As it was already mentioned in the comments, your application runs in another thread and it is never terminating since the stream is infinite, so you will have to manually terminate it when a signal like SIGTERM or SIGINT is received by the application (it's emitted whenever you hit ctrl+c to terminate the app).

You could do something like this:

  1. create an instance of Deferred
  2. Use it to trigger interruptWhen after any of TERM or INT signal is received.

For example:

import sun.misc.Signal

object FS2 extends IOApp {

  override def run(args: List[String]): IO[ExitCode] = for {
    cancel <- Deferred[IO, Either[Throwable, Unit]] //deferred used as flat telling if terminations signal
                                                    //was received
    _ <- (IO.async_[Unit]{ cb =>
      Signal.handle(
        new Signal("INT"), //INT and TERM signals are nearly identical, we have to handle both
        (sig: Signal) => cb(Right(()))
      )
      Signal.handle(
        new Signal("TERM"),
        (sig: Signal) => cb(Right(()))
      )
    } *> cancel.complete(Right(()))).start //after INT or TERM signal is intercepted it will complete
                                           //deferred and terminate fiber
                                           //we have to run method start to run waiting for signal in another fiber
                                           //in other case program will block here
    app <- Stream.awakeEvery[IO](1.seconds).map { _ => //your stream
      println("hello")
    }.interruptWhen(cancel).compile.drain.as(ExitCode.Error)  //interruptWhen ends stream when deferred completes
  } yield app

}

This version of the app will terminate whenever you hit ctrl + c in sbt shell.

Ben Hutchison
  • 2,433
  • 2
  • 21
  • 25
Krzysztof Atłasik
  • 21,985
  • 6
  • 54
  • 76
  • Please include imports and version details. I can't get this code to compile with FS2 3.2.3 – Ben Hutchison Jan 02 '22 at 23:15
  • Be warned that a signal handler like above will "eat" the signal and prevent SBT default handling. So if there is a bug in your interrupt logic, eg not interrupting _all_ your streams, then the program won't exit and SBT will be unaware the signal fired. I ended up going the fork route `Compile / run / fork := true` – Ben Hutchison Jan 02 '22 at 23:38