Skip to content

Scala Futures

Cleary, Paul edited this page Nov 9, 2015 · 1 revision

Scala Futures are an elegant, non-blocking way to write your code in order to maximize parallelism. However, they present significant challenges to maintaining a Trace Context across the code. But, no worries! Money can help!

Money includes a concurrent package that provides support to propagate (and extend) Trace Contexts across Scala Futures. It will manage your Trace Contexts seamlessly across Execution Contexts!

It is rather simple to use, pay attention to the namespaces that are imported:

Starting and Stopping Spans

To start and stop spans in Scala Futures, you need to use the com.comcast.money.concurrent.Futures API.

The following snippet of code will wrap the execution of the block provided in a future. A span will automatically be started before the block starts, and will be guaranteed to be stopped once the block of code is complete (the Future is complete):

import com.comcast.money.concurrent.Futures
import com.comcast.money.core.Money.tracer
import scala.concurrent.ExecutionContext.Implicits.global

def withMoney(indexSize:Long):Future[String] = newTrace("future-span") {
  tracer.record("index-size", indexSize)
}

The withMoney method returns a Future, that future is created inside the Futures.newTrace function. The string value that is specified in the newTrace function is the name of the span that will be emitted.

Extending an existing trace to another future

In order to propagate an existing Trace Context across separate futures, you need to use the Futures.continueTrace API. This will make sure that the execution of the new future happens within the existing Trace Context.

To show this, let's build on our previous example:

import com.comcast.money.concurrent.Futures
import com.comcast.money.core.Money.tracer
import scala.concurrent.ExecutionContext.Implicits.global

def withMoney(indexSize:Long):Future[String] = newTrace("future-span") {
  tracer.record("index-size", indexSize)
  keepGoing("man")
}

def keepGoing(here:String):Future[String] = continueTrace {
  tracer.record("continued-value", here)
}

In this example, the block inside continueTrace is a Future, and will be executed on a separate thread. By using continueTrace here, the "continued-value" note will be recorded on the "future-span"! Amazing!

Supporting Functional Combinators and other Scala Future Fun!

All of the functional combinators that come with Scala Futures are supported as well, so you can do fun things like map, flatMap, collect, transform, foreach, etc. are all supported by Money Futures...

val fut = newTrace("crazy-train") {
  tracer.record("begin", "how")  
  100
}.map { v =>
  tracer.record("map", "do")
  v
}.collect { case v =>
  tracer.record("collect", "you")
  v
}.map { v =>
  tracer.record("map2", "like?")
  200
}

Yes, as crazy as that looks, all of the notes: "begin", "map", "collect", and "map2" will all be captured on the "crazy-train" span.

How about nested Futures?

We can do that too...

      val fut = newTrace("root") {
        tracer.record("begin", "root")

        // Here, we have a nested, child span inside the root parent
        newTrace("nested") {
          tracer.record("begin", "nested")
          Some(456)
        }.flatMap { case Some(v) =>
          tracer.record("flatMap", "nested")
          Future {v}
        }.map { case v =>
          tracer.record("map", "nested")
          v
        }
      }.flatMap { child =>
        tracer.record("flatMap", "root")
        child
      }.map { case s =>
        tracer.record("map", "root")
      }

All of the correct notes will be captured on all of the correct spans. You can nest as deeply as you want, and the appropriate parent-child relationships will all be maintained.

Part Black Magic, part Witchcraft.

Out-Of-Control Futures

Sometimes, you cannot control the creation of a Scala Future, as it is surfaced from another library that you do not control (I'm looking at you Play Framework WS API

When you are faced with this scenario, you need to pull the future from the third party library into the existing trace context (span). We do that with the wrapTrace method in the futures API:

def wrapTrace[T](f: => Future[T])(implicit ec: ExecutionContext): Future[T]

Notice you need to provide f which yields a Future. Here is how to use it in an example:

def withMoney(indexSize:Long):Future[String] = newTrace("future-span") {
  // we just created a new span, establishing a trace context in this future

  // Future bubbles up from WS API, we need to wrap this in a trace
  wrapTrace { 
    WS.url(url).get().map {
      response =>
        (response.json \ "thing" \ "jawn").as[String]
    }
  } onComplete {
    case _ => tracer.record("done", "man") 
  }
}

In the above example, we first create a new span called future-span. In side there, we need to make a call using the Play WS API which surfaces a Future. In order to make sure that our note "done" -> "man" is recorded in onComplete, we must make sure that we use wrapTrace round the WS API call.