Locals in scala.concurrent

[Following up on https://github.com/scala/bug/issues/9835]

Scala seems to lack a blessed way to propagate request-scoped values through an execution path. Canonical use case is to propagate a traceId for log correlation, and when sent to upstream services via http header or similar, for distributed tracing. I’m aware of 4 approaches:

  1. Use the call stack to pass it, perhaps using an implicit parameter. Pro: it’s straightforward and easy to reason about. Con: Will only work with code that declares the parameter, typically user code and not library code. This is the approach the Go designers advocate via context.Context.

  2. Inject an overloaded ExecutionContext.prepare() that propagates ThreadLocal Storage (TLS) across threads, as originally demonstrated here https://yanns.github.io/blog/2014/05/04/slf4j-mapped-diagnostic-context-mdc-with-play-framework/ Pro: no change to user code. Con: fragile if app does not have complete control of ExecutionContexts in use, and prepare() is now deprecated.

  3. Kamon, and I think Cinnamon, are similar to above in their use of TLS, but work at runtime via bytecode weaving to modify Future internals to inject the propagation. Pro: no change to user code. Con: bytecode weaving and TLS complexity, Cinnamon is not free to use.

  4. Twitter/Finagle locals also use TLS, but their Future implementation propagates them across thread boundaries. I don’t have hands-on experience with this approach. Pro: SDK support. Con: approach seems good but requires commitment to Finagle stack.

I believe the Twitter approach was considered when designing scala Futures, but not sure why it was rejected. I’m curious what the reasons were, and if a similar mechanism would be considered at some point.

And more generally, can the Scala community promote a recommended approach for context propagation? Given the tradeoffs of current choices, I don’t see an obvious best answer, and I think better guidance would lead to user code and libraries and tools working together to deliver runtime observability of multi-threaded and distributed systems.

jeff

4 Likes

For reference, in pure FP, Julien Tournay/Pascal Voitot (@mandubian) built “precepte” for that purpose: https://github.com/MfgLabs/precepte

There was a talk on it at scala.io 2016, that seems quite powerful. They used it to gather metrics on the code.

I don’t think precepte is widely used, but the approach iwas nice.

Monix has TaskLocal now.

Ideally, ExecutionContext.prepare() will be removed. It is deprecated, but cannot yet be removed, as there is no replacement. It is possible that this could be a replacement.

I have a feeling that a Local implementation will require “privileged” support from within Future/ExecutionContext. It may be worth looking into what Twitter/Finagle does, at least as inspiration.

As @hepin1989 already mentioned, Monix’s Local might be a good solution here - looks very promising! Here’s a blog showing how to use it with Logback’s MDC: http://olegpy.com/better-logging-monix-1/

Adam

1 Like

Monix Local does look interesting, similar in spirit to the Finagle solution, and I think with similar consequence of requiring a Monix Task-oriented codebase instead of a Future-oriented codebase.

jeff

Yes, true, you need to base your application on Task. But Monix becomes a more and more compelling alternative to Future-based stacks, esp with the tight integration w/ cats & cats-effect.

Plus in the end, you can write a lot of your business logic in a container-independent way by writing abstract methods with the resulting container constrained on only the effects you need (“tagless final”)

2 Likes

Agree, the Monix-based stack becomes more and more compelling alternative to Future-based stacks, which is a good segue to bringing the thread back to the original issue that was opened in 2016… Can we get something like Twitter/Finagle Locals in scala.concurrent?

Is this feature still wanted or everybody have moved to Monix? Twitter’s implementation looks pretty simple, requiring ExecutionContext modification to propagate TLS between threads.