I expected that canceling Fiber makes it joinable. Here is an example of what I mean:
object TestFiber extends App {
implicit val contextShift: ContextShift[IO] =
IO.contextShift(ExecutionContext.global)
implicit val timer: Timer[IO] = IO.timer(ExecutionContext.global)
val test = for {
fiber <- IO.never.attempt.start
_ <- fiber.cancel
_ <- fiber.join
_ <- IO(println("Finished"))
} yield ()
test.unsafeRunSync() //blocks instead of printing "Finished" and exit
}
I expected that after cancel, join would immediately return and Finished would be printed.
But the actual behavior is that the program hangs up. How to make a Fiber finished so it can be joinable?
This seems to be the intended, documented behaviour of cats-effect. The ZIO equivalent to cancel is interrupt, and it returns an Exit type that will allow you to handle all possible cases (success, error, interruption) very easily, so if possible, I'd encourage you to just switch to ZIO.
But if you need to use cats-effect, the best that I could come up with (and it's not very good) is to use a Deferred:
def catchCancel[F[_]: Concurrent, A](io: F[A]): F[(F[Unit], F[Either[Option[Throwable], A]])] =
for {
deferred <- Deferred[F, Either[Option[Throwable], A]]
fiber <- (for {
a <- Concurrent[F].unit.bracketCase(_ => io) {
case (_, ExitCase.Completed) => Concurrent[F].unit
case (_, ExitCase.Error(e)) => deferred.complete(Left(Some(e))).as(())
case (_, ExitCase.Canceled) => deferred.complete(Left(None))
}
_ <- deferred.complete(Right(a))
} yield ()).start
} yield (fiber.cancel, deferred.get)
And you can use it like so:
val test = for {
x <- catchCancel[IO, Nothing](IO.never)
(cancel, getResult) = x
_ <- cancel
result <- getResult
_ <- IO(println(s"Finished $result"))
} yield ()
But seriously, why would you inflict this onto yourself? Just switch to ZIO, it fixes this problem and a boatload of others.
Related
I have a stream that just repeats every x seconds.
In my unit tests I want to test certain business logic, so I need my clock to start at 1:30pm and run until 1:45pm.
How can I mock this type of behaviour?
import java.time.{ZoneId, ZonedDateTime}
val zoneId = ZoneId.of("America/New_York")
Stream
.repeatEval {
for {
realTime <- Clock[F].realTimeInstant
zdt = ZonedDateTime.ofInstant(realTime, zoneId)
_ <- std.Console[F].println(s"zdt=$zdt")
} yield ()
}
You can use the TestControl structure from cats-effect-testkit to have fine-grained control over time in tests. It provides a full IORuntime, so your code just operates as normal
See the example of advancing the clock manually:
test("backoff appropriately between attempts") {
case object TestException extends RuntimeException
val action = IO.raiseError(TestException)
val program = Random.scalaUtilRandom[IO] flatMap { random =>
retry(action, 1.minute, 5, random)
}
TestControl.execute(program) flatMap { control =>
for {
_ <- control.results.assertEquals(None)
_ <- control.tick
_ <- 0.until(4) traverse { i =>
for {
_ <- control.results.assertEquals(None)
interval <- control.nextInterval
_ <- IO(assert(interval >= 0.nanos))
_ <- IO(assert(interval < (1 << i).minute))
_ <- control.advanceAndTick(interval)
} yield ()
}
_ <- control.results.assertEquals(Some(Outcome.failed(TestException)))
} yield ()
}
}
Im trying to alter ZIO's example code to fit what I want, but ran into a problem. I want to implement functionalities between different rpc calls, but I can't seem to get it to work, since in below example, only the while loop, rcpMethod3() and rcpMethod4() gets executed, whereas rcpMethod1() and rcpMethod2() doesn't.
I want to execute all of the rcpMethods and the while loop.
object Client extends App {
val clientLayer: Layer[Throwable, ClientZLayer] =
ClientZLayer.live(
ZManagedChannel(
ManagedChannelBuilder.forAddress("localhost", 8980).usePlaintext()
)
)
// rcp methods
def rcpMethod1(): ZIO[ClientZLayer with Console, Status, Unit] = {
for {
response <- ClientZLayer.rcpMethod1(Greeting("Hi"))
_ <- putStrLn(s"""Greeted: "${response.name}".""")
} yield ()
}
// Run the code
final def run(args: List[String]) = {
(for {
_ <- rcpMethod1()
_ <- rcpMethod2()
} yield ()).provideCustomLayer(clientLayer).exitCode
while(condition) {
// bla bla
}
(for {
_ <- rcpMethod3()
_ <- rcpMethod4()
} yield ()).provideCustomLayer(clientLayer).exitCode
}
}
The ZIO data type is a functional effect. A functional effect is a description of a workflow. This is why we have the run method at the end of the world. This run method executes the provided ZIO effect.
All the rcpMethodN methods are ZIO effect, so they are just a description of running workflow. If you want to run these effects sequentially you should compose them using for-comprehension or flatMap like this:
for {
_ <- rcpMethod1()
_ <- rcpMethod2()
_ <- rcpMethod3()
_ <- rcpMethod4()
} yield ()
The while(condition){ ... } is another mistake in your code. You should introduce these loop structs with ZIO data type. For example:
for {
_ <- rcpMethod1()
_ <- rcpMethod2()
_ <- ZIO.effect(???).repeatWhile(condition)
_ <- rcpMethod3()
_ <- rcpMethod4()
} yield ()
help me how to organize a directory scan on ZIO. This is my version, but it doesn't track all file creation events (miss some events).
object Main extends App {
val program = for {
stream <- ZIO.succeed(waitEvents)
_ <- stream.run(ZSink.foreach(k => putStrLn(k.map(e => (e.kind(), e.context())).mkString("\n"))))
} yield ()
val managedWatchService = ZManaged.make {
for {
watchService <- FileSystem.default.newWatchService
path = Path("c:/temp")
_ <- path.register(watchService,
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_DELETE
)
} yield watchService
}(_.close.orDie)
val lookKey = ZManaged.make {
managedWatchService.use(watchService => watchService.take)
}(_.reset)
val waitEvents = ZStream.fromEffect {
lookKey.use(key => key.pollEvents)
}.repeat(Schedule.forever)
override def run(args: List[String]): ZIO[zio.ZEnv, Nothing, ExitCode] =
program
.provideLayer(Console.live ++ Blocking.live ++ Clock.live)
.exitCode
}
Thank you for your advice.
You are forcing your WatchService to shutdown and recreate every time you poll for events. Since that probably involves some system handles it is likely fairly slow so you would probably missing file events that occur in between. More likely you want to produce the WatchService once and then poll it repeatedly. I would suggest something like this instead:
object Main extends App {
val managedWatchService = ZManaged.make {
for {
watchService <- FileSystem.default.newWatchService
path = Path("c:/temp")
_ <- path.register(watchService,
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_DELETE
)
} yield watchService
}(_.close.orDie)
// Convert ZManaged[R, E, ZStream[R, E, A]] into ZStream[R, E, A]
val waitEvents = ZStream.unwrapManaged(
managedWatchService.mapM(_.take).map { key =>
// Use simple effect composition instead of a managed for readability.
ZStream.repeatEffect(key.pollEvents <* key.reset)
// Optional: Flatten the `List` of values that is returned
.flattenIterables
}
)
val program = waitEvents
.map(e => (e.kind(), e.context()).toString)
.foreach(putStrLn).unit
override def run(args: List[String]): ZIO[zio.ZEnv, Nothing, ExitCode] =
program
.provideLayer(Console.live ++ Blocking.live ++ Clock.live)
.exitCode
}
Also as a side note, when using ZManaged, you probably don't want to do
ZManaged.make(otherManaged.use(doSomething))(tearDown)
because you will cause the finalizers to execute out of order. ZManaged can already handle the ordering of teardown just through normal flatMap composition.
otherManaged.flatMap { other => ZManaged.make(doSomething(other))(tearDown) }
What is the proper behavior to avoid resource/memory leak? Is it required/correct to cancel failed Fiber?
val someEffect: F[Unit] = //...
def someFiber[F[_]: MonadError[?[_], Throwable]](fa: F[Fiber[F, Unit]]): F[Unit] =
for {
fiber <- fa
_ <- fiber.join handleErrorWith { _ =>
//Is this cancel required?
fiber.cancel *> someEffect
}
} yield ()
My concern is about cancel when handling Fiber.join error? Is it required?
Consider the following code inside a Play Framework controller:
val firstFuture = function1(id)
val secondFuture = function2(id)
val resultFuture = for {
first <- firstFuture
second <- secondFuture(_.get)
result <- function3(first, second)
} yield Ok(s"Processed $id")
resultFuture.map(result => result).recover { case t => InternalServerError(s"Error organizing files: $t.getMessage")}
Here are some details about the functions:
function1 returns Future[List]
function2 returns Future[Option[Person]]
function1 and function2 can run in parallel, but function3 needs the results for both.
Given this information, I have some questions:
Although the application is such that this code is very unlikely to be called with an improper id, I would like to handle this possibility. Basically, I would like to return NotFound if function2 returns None, but I can't figure out how to do that.
Will the recover call handle an Exception thrown any step of the way?
Is there a more elegant or idiomatic way to write this code?
Perhaps using collect, and then you can recover the NoSuchElementException--which yes, will recover a failure from any step of the way. resultFuture will either be successful with the mapped Result, or failed with the first exception that was thrown.
val firstFuture = function1(id)
val secondFuture = function2(id)
val resultFuture = for {
first <- firstFuture
second <- secondFuture.collect(case Some(x) => x)
result <- function3(first, second)
} yield Ok(s"Processed $id")
resultFuture.map(result => result)
.recover { case java.util.NoSuchElementException => NotFound }
.recover { case t => InternalServerError(s"Error organizing files: $t.getMessage")}
I would go with Scalaz OptionT. Maybe when you have only one function Future[Optipn[T]] it's overkill, but when you'll start adding more functions it will become super useful
import scala.concurrent.ExecutionContext.Implicits.global
import scalaz.OptionT
import scalaz.OptionT._
import scalaz.std.scalaFuture._
// Wrap 'some' result into OptionT
private def someOptionT[T](t: Future[T]): OptionT[Future, T] =
optionT[Future](t.map(Some.apply))
val firstFuture = function1(id)
val secondFuture = function2(id)
val action = for {
list <- someOptionT(firstFuture)
person <- optionT(secondFuture)
result = function3(list, person)
} yield result
action.run.map {
case None => NotFound
case Some(result) => Ok(s"Processed $id")
} recover {
case NonFatal(err) => InternalServerError(s"Error organizing files: ${err.getMessage}")
}