0

I have written a small utility wrapper around the Scala ExecutionContext to enable transfer of MDC contexts across Future instances. It works as expected but an undesired side-effect is that we now don't seem to get stack traces that go across Futures. How can I ensure that the stack trace is propagated along with the MDC?

Here's my code for reference:

import org.slf4j.MDC

import scala.concurrent.ExecutionContext
import scala.jdk.CollectionConverters._


object MdcOps {

  implicit class ExecutionContextExt(value: ExecutionContext) {
    def withMdc: ExecutionContext = new MdcExecutionContext(value)
  }

  def withMdc[A](mdc: Map[String, Any], replace: Boolean)(doIt: => A): A = {
    val currentMdcContext = getMdc
    val newMdc = if(replace) mdc else mdc ++ currentMdcContext
    try { setMdc(newMdc); doIt }
    finally { setMdc(currentMdcContext) }
  }

  def setMdc(mdc: Map[String, Any]): Unit = {
    if(mdc.isEmpty) {
      MDC.clear()
    } else
      MDC.setContextMap(mdc.view.mapValues(_.toString).toMap.asJava)
  }

  def getMdc: Map[String, String] = Option(MDC.getCopyOfContextMap).map(_.asScala.toMap).getOrElse(Map.empty)
}

class MdcExecutionContext(underlying: ExecutionContext, context: Map[String, String] = Map.empty) extends ExecutionContext {
  override def prepare(): ExecutionContext = new MdcExecutionContext(underlying, MdcOps.getMdc)
  override def execute(runnable: Runnable): Unit = underlying.execute { () =>
    MdcOps.withMdc(context, replace = true)(runnable.run())
  }
  override def reportFailure(t: Throwable): Unit = underlying.reportFailure(t)
}
tjarvstrand
  • 836
  • 9
  • 20

1 Answers1

1

In general it is hard. ZIO implemented it - see: https://www.slideshare.net/jdegoes/error-management-future-vs-zio from slide 53 to see the result - but creation of stacks, and passing it around causes performance penalty x2.4.

Since you cannot change API to e.g. generate stack trace in compile times using macros (using e.g. sourcode library), on each .map/.flatMap you would have to create a stack trace, remove irrelevant frames from it, maybe combine it with frames from previous context and set it in MDC. Putting technical details aside - this is hard to get right and heavy weight, more like a material for a whole library than simple utility. If it could be done cheaply this would be a build in.

If you are very interested in it, either pick ZIO or analyze how it implements it - from what I see it required a lot of effort and building this functionality into the library, and even then it causes performance penalty. I can only imagine that penalty by using JVM stack traces and passing them around would be even greater.

Mateusz Kubuszok
  • 24,995
  • 4
  • 42
  • 64
  • Not the answer I was hoping for, but thanks nonetheless! Would it be possible to work around it by somehow catching "inner" exceptions and wrap them in exceptions with the "outer" stacktrace? – tjarvstrand Jun 01 '20 at 13:11
  • I made some attempt to handle this some time ago - it basically required me to: take implicit from [sourcecode](https://github.com/lihaoyi/sourcecode) in my own wrapper around Future/IO on `.map`, `.flatMap`, then use it enrich exceptions thrown with some context. But I have some serious doubts about such approach and I cannot say I would recommend anyone else to use it. – Mateusz Kubuszok Jun 01 '20 at 13:23