Local Variable Propagation with Monix Part 1

Leandro Bolívar
Pragmatic Scala
Published in
3 min readFeb 25, 2018

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 its ThreadLocal.get or ThreadLocal.set method) has its own, independently initialized copy of the variable.
  • monix.execution.misc.Local: A Local is a ThreadLocal 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 a ThreadLocal that is pure and with a flexible scope, being processed in the context of the Task 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.

--

--

Leandro Bolívar
Pragmatic Scala

The question isn't who is going to let me; it's who is going to stop me. Ayn Rand.