Saturday, January 16, 2021

Background processing with Scala Cats Effect



Running any tasks with Scala Future in the background can be done using the following:

Snippet 1

  import scala.concurrent.duration._
  import scala.concurrent.ExecutionContext.Implicits.global
  import scala.concurrent.{Await, Future}

  val future1 = Future { Thread.sleep(4000); println("Work 1 Completed") }
  val future2 = Future { Thread.sleep(1000); println("Work 2 Completed") }

  val future3 =
    for {
      _ <- future1
      _ <- future2
    } yield ()

  Await.result(future3, Duration.Inf)

And if one has been working with Scala Future it'd be obvious that as soon as lines with variables future1 and future2 are executed the body inside the Future starts executing immediately, i.e the body of the Future is eagerly evaluated by submitting it for execution on a different Java Thread using the Implicit ExecutionContext available in the scope via the import. Now with the help of the for-comprehension, which is nothing but a combination of flatMap, the Futures are composed together to yield a Unit. And finally, Awaiting on future3 gets the result when both future1 and future2 are completed successfully (here Await is just used for demonstration purposes).

But if the above code snippet is changed a bit to the following:

Snippet 2

  val future =
    for {
      _ <- Future { Thread.sleep(4000); println("Work 1 Completed") }
      _ <- Future { Thread.sleep(1000); println("Work 2 Completed") }
    } yield ()

  Await.result(future, Duration.Inf)

both Futures will be executed sequentially because the second Future is not initialized/executed until the first Future is completed, i.e the above for-comprehension is the same as the following:

Snippet 3

  Future {
    Thread.sleep(4000); println("Work 1 Completed")
  }.flatMap { _ =>
      Future {
        Thread.sleep(1000); println("Work 2 Completed")
      }
    }

Similar results can be achieved using Cats Effect IO with the added benefit of referential transparency (note that the Scala Future is not referentially transparent).

Cats Effect version similar to Snippet 2 will look something like the following:

Snippet 4

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

object Test extends IOApp {

  def work[A](work: A, time: FiniteDuration): IO[Unit] =
    IO.sleep(time) *> IO(work)
      .flatMap(completedWork => IO(println(s"Done work: $completedWork")))

  val program: IO[Unit] =
    for {
      _ <- work("work 1", 4.second)
      _ <- work("work 2", 1.second)
    } yield ()

  override def run(args: List[String]): IO[ExitCode] =
    program *> IO.never
}

In the above case, the second IO (inside for-comprehension) is not evaluated until the first IO is completed, i.e the IOs are run sequentially and the order in which the print line statements will be executed is "Done work: work 1" and then "Done work: work 2".

The Cats Effect version similar to Snippet 1 will look something like the following:

Snippet 5

val program1 = work("work 1", 4.second).start
val program2 = work("work 2", 1.second).start

val program: IO[Unit] =
  for {
    _ <- program1
    _ <- program2
    _ <- IO(println("for-comprehension done!"))
  } yield ()

wherein the order in which the print line statements will be executed is "for-comprehension done!" then "Done work: work 2" and then "Done work: work 1". Here, the start method uses ContextShift instead of Scala's ExecutionContext directly.

start returns a Fiber which can be canceled, and the following code snippet will cancel the Fiber returned by program1 as soon as the program2 is evaluated. 

Snippet 6

  val program1 = work("work 1", 4.second).start
  val program2 = work("work 2", 1.second).start

  val program: IO[Unit] =
    for {
      fiber1 <- program1
      _      <- program2
      _      <- fiber1.cancel
      _      <- IO(println("for-comprehension done!"))
    } yield ()

and in this case, the following will be the probable output on the console: "for-comprehension done!" then "Done work: work 2" and "Done work: work 1" won't be printed. "Probable" because the by the time program1 gets a chance to complete, fiber1.cancel line will be executed that'll cancel the execution of program1.








No comments:

Post a Comment