5

Question: Is it safe to suspend unsafeRunSync with IO? E.g.

val io: IO[Unit] = //...
val io2: IO[Unit] = IO(io.unsafeRunSync)

The reason I would do so is that I have some class parameterized with F[_]: Effect which is like a cache:

import cats.effect.Effect

final class MyChache[F[_]](implicit F: Effect[F]) {
  private val cache = new ConcurrentHashMap[Int, String]

  def getOrCreate(key: Int): F[String] = F delay {
    cache.computeIfAbsent(
      key, 
      k => longRunningEffecfulComputation(k).toIO.unsafeRunSync() // <-- Here
    )
  }
}


object MyCache {
  def longRunningEffecfulComputation[F[_] : Effect](key: Int): F[String] = {
    //...
  }
}

The point is I want to run this long running effectfull computation only once for each key (it's pretty infrequent). Yet I would like to stay non-blocking when retrieving existing key.

ConcurrentHashMap seems to be a perfect choice, but it requires this ugly trick with running and suspending the effect. Is there a better way to go?

Some Name
  • 8,555
  • 5
  • 27
  • 77
  • 3
    If it's actually a long-running computation, it probably shouldn't go in `computeIfAbsent` anyway. – Travis Brown Mar 02 '19 at 21:50
  • 1
    Perhaps if the operation is effectful and long running, you'd want a `ConcurrentHashMap[Int, F[String]]`. If you go all through all the trouble of using cats-effect, strive to use it uniformly across the code base. Having an operation blow on you with `unsafeRunSync` can be unexpected. – Yuval Itzchakov Mar 03 '19 at 09:53
  • @YuvalItzchakov Having `ConcurrentHashMap[Int, F[String]]` could be a solution if I did not have a requirements of computing the value only once per key. Problably, there can be some trick with `ConcurrentHashMap[Int, F[MVar[F, String]]]`, but I'm not sure... I think there is a design issue with all this code. – Some Name Mar 03 '19 at 11:03
  • @TravisBrown With `ConcurrentHashMap` I can be sure that the computation is done only once per key and when it is performed we have no other bucket locked. That was the actual reason behind using `ConcurrentHashMap::computeIfAbsent`. I could not find something more pure yet still practical. – Some Name Mar 03 '19 at 11:06
  • @SomeName Why is that a problem though? Even if you're in the context of `F[String]`, it should only be computed once assuming no one cheats with `unsafeRunSync`. – Yuval Itzchakov Mar 03 '19 at 11:08
  • @YuvalItzchakov I tried to declare the `val cache = new ConcurrentHashMap[Int, F[String]]` and then re-write `getOrCreate(key: Int) = F suspend { cache.computeIfAbsent(key, k => longRunningEffecfulComputation(k)) }`. And it turns out that the `longRunningEffecfulComputation` is evaluated any time the key is added (even though it is already there). – Some Name Mar 03 '19 at 12:07
  • @SomeName How did you call it? – Yuval Itzchakov Mar 03 '19 at 15:22

1 Answers1

1

This is at least potentially unsafe. Suppose that your long running computation used a fixed-size thread pool:

import java.util.concurrent.Executors
import scala.concurrent.ExecutionContext
import cats.effect.Async

object MyCache {
  val smallThreadPool = ExecutionContext.fromExecutor(Executors.newFixedThreadPool(1))

  def longRunningEffectfulComputation[F[_] : Effect](key: Int): F[String] = {
    Effect[F].flatMap(Async.shift[F](smallThreadPool))(_ => Effect[F].delay("test"))
  }
}

And your cache was used on the same thread pool:

val io = for {
  _ <- IO.shift(MyCache.smallThreadPool)
  x <- new MyCache[IO].getOrCreate(1)
} yield x

When you call io.unsafeRunSync(), you will see that it does not terminate.

Instead, you can use a cache api that supports cats-effect, like ScalaCache.

Brian McCutchon
  • 8,354
  • 3
  • 33
  • 45