I'm looking for a way in which a Task
(i.e. outer scope) could execute a "subTask" using flatMap
or something equivalent and make sure that any subsequent chained calls in the outer scope use the original scheduler.
Libraries and scala used:
- scala - 2.12.4
- monix -
"io.monix" %% "monix" % "3.0.0-RC1"
- cats -
"org.typelevel" %% "cats-core" % "1.0.1"
Example code:
import monix.eval.Task
import monix.execution.Scheduler
import scala.concurrent.Await
import scala.concurrent.duration.Duration
import monix.execution.Scheduler.Implicits.global
import cats.implicits._
object Test extends App {
val io1 = Scheduler.io("io1")
val io2 = Scheduler.io("io2")
def taskEval(name: String) = Task.eval(println(s"Running eval Task [$name] on thread [${Thread.currentThread().getName}]"))
def subTask: Task[Unit] = {
taskEval("subTaskScope").executeOn(io2)
}
def outerScope(sub: Task[Unit]): Task[Unit] = {
taskEval("outerScopeBefore") *> sub *> taskEval("outerScopeAfter")
}
def outerScopeTryProtect(sub: Task[Unit]): Task[Unit] = {
taskEval("outerScopeBefore") *> (sub <* Task.shift) *> taskEval("outerScopeAfter")
}
val program1 = taskEval("programBefore").executeOn(io1) *> outerScope(subTask) *> taskEval("programAfter")
val program2 = taskEval("programBefore").executeOn(io1) *> outerScopeTryProtect(subTask) *> taskEval("programAfter")
Await.result(program1.runAsync, Duration.Inf)
// Running eval Task [programBefore] on thread [io1-573]
// Running eval Task [outerScopeBefore] on thread [io1-573]
// Running eval Task [subTaskScope] on thread [io2-574]
// Running eval Task [outerScopeAfter] on thread [io2-574] // << we don't shift back so we are stuck with the scheduler that is forces by subTask
// Running eval Task [programAfter] on thread [io2-574]
println("------")
// Running eval Task [programBefore] on thread [io1-573]
// Running eval Task [outerScopeBefore] on thread [io1-573]
// Running eval Task [subTaskScope] on thread [io2-574]
// Running eval Task [outerScopeAfter] on thread [scala-execution-context-global-575] // we shift the scheduler but this restores the default scheduler
// Running eval Task [programAfter] on thread [scala-execution-context-global-575]
Await.result(program2.runAsync, Duration.Inf)
}
The subTask
method wants to do some asynchronous work on a dedicated scheduler (io2
) so it forces async boundary an scheduler using executeOn
.
The outerScope
method is being executed in some program program1
and it calls the sub
(i.e. subTask
) using flatMap
. Since it does not to any explicit async boundary, if the subTask
happens to change the scheduler (which it does), the rest of the outerScope
will use the scheduler changed by the subTask
. For this reason the call to taskEval("outerScopeAfter")
is executed on the io2
scheduler.
The outerScopeTryProtect
tries to protect the scheduler it uses by introducing an async boundary (using Task.shift
) after the flatMap
ped sub
(i.e. subTask
). However, the async boundary (Task.shift
) resets the scheduler to the default scheduler which will in this case go all the way back to the one used implicitly in program2.runAsync
. This is not what we want, as we would like to be back to the scheduler that was used when calling taskEval("outerScopeBefore")
, i.e. scheduler io1
.
What I'm looking for is something like Task[A].flatMap[B](f: A => Task[B]): Task[B]
that would execute the task produced by f
in any way the f
specifies (possibly using a different scheduler), but the resulting Task
of the flatMap
call will be back to the scheduler used by Task[A]
before the flatMap
.