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:
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.
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.
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.
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.
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.
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”)
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.