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?

7
  • 3
    If it's actually a long-running computation, it probably shouldn't go in computeIfAbsent anyway. Commented Mar 2, 2019 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. Commented Mar 3, 2019 at 9: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. Commented Mar 3, 2019 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. Commented Mar 3, 2019 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. Commented Mar 3, 2019 at 11:08

1 Answer 1

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.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.