Local Variable Propagation with Monix Part 1
I have been using monix for over a year and a half in the projects I have been involved with and it has proven to be a reliable tool for developing reactive applications due to the stack safety guarantees it provides and the simplicity it offers for composing asynchronous tasks in a functional approach. By the time this article was written, monix is in its third milestone release for version 3.0.0 which adds a feature that was long due: local variable propagation.
The challenge of local variable propagation does not lie in implementing the Local
itself but on how it should get passed throughout the Task
execution. One of the main features of Task
is stack safety by ensuring trampolined execution and adds as an optimization the deferral of async boundaries, this means, in simple terms, that the given task will run in the current thread until it forces an async boundary. This complicates the passing of the local variable in case that boundary is reached because of the way TaskRunLoop
manages its own execution. If we would want to propagate a local var using scala Future
we could have a similar approach like the one explained here, which will delegate the job to the ExecutionService
you implement. For Task
, the solution does not lie there and we had to dig deeper to achieve our goal. To sum it up, we had to set the passing of the Local
variable when we detect the async boundary has been reached, which specifically we do here and then in the final callback.
We have three building blocks for task local propagation, as stated in the javadocs:
- monix.execution.misc.ThreadLocal: Cross-platform equivalent for
java.lang.ThreadLocal
, for specifying thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via itsThreadLocal.get
orThreadLocal.set
method) has its own, independently initialized copy of the variable. - monix.execution.misc.Local: A
Local
is aThreadLocal
whose scope is flexible. The state of all Locals may be saved or restored onto the current thread by the user. This is useful for threading Locals through execution contexts. - monix.eval.TaskLocal: A
TaskLocal
is like aThreadLocal
that is pure and with a flexible scope, being processed in the context of theTask
data type.
Basically we have aLocal
that handles the scope of a ThreadLocal
and it is lifted to the Task
context with theTaskLocal
. Now we can define local variables under the execution of a task with the guarantee that it will propagate the variable when an async boundary occurs. The tests to illustrate the behavior are as follows:
Every Task.shift
implies a forced async boundary so we could assert that in fact the variable is being properly propagated under each scenario. Notice that the propagation of the variables is not by default, we have to define an implicit val opts = Task.defaultOptions.enableLocalContextPropagation
in scope so that the task.runAsyncOpt
is enabled to pass the local in case an async boundary occurs.
I have defined additional tests to clarify some doubts I had regarding the execution and cleanup of the local vars:
This is but a summarized look at TaskLocal
, now the next step is to define a practical implementation that can facilitate the definition of contexts and the setting and cleaning up of the locals. A proposal will come in a follow up post once I polish some details.