Nik Shkrob

@nshkrob

Posted September 10, 2015

Services-per-endpoint in Scrooge

We’ve released Service-per-endpoint support for Scala Thrift clients generated by Scrooge. This change has been introduced in Scrooge 4.0.0.

This allows the use of Finagle filters with Thrift services to do retries, timeouts, etc. in a Finagle-idiomatic way. Scrooge generates a Service for each Thrift method, so different filter chains can be built for different methods. Typical examples of this are per-method timeout and retry policies.

An example

Here’s a Thrift service definition:

service LoggerService {
  string log(1: string message, 2: i32 logLevel);
  i32 getLogSize();
}

Before this patch, Scrooge would generate this method-based interface:

trait FutureIface extends LoggerService[Future] {
  def log(message: String, logLevel: Int): Future[String]
  def getLogSize(): Future[Int]
}

With the patch, Scrooge also generates the following:

case class ServiceIface(
  log: Service[Log.Args, Log.Result],
  getLogSize: Service[GetLogSize.Args, GetLogSize.Result])

Note that every method in the IDL becomes a Service for the corresponding Args and Result structures. The wrappers are needed to wrap multiple method arguments into one type.

To construct a client, we use newServiceIFace:

val clientServiceIface: LoggerService.ServiceIface =
  Thrift.newServiceIface[LoggerService.ServiceIface]("localhost:1234")

Because every Thrift method is a Finagle Service, you can decorate them with Filters:

val uppercaseFilter = new SimpleFilter[Log.Args, Log.Result] {
  def apply(req: Log.Args, service: Service[Log.Args, Log.Result]): Future[Log.Result] = {
    val uppercaseRequest = req.copy(message = req.message.toUpperCase)
    service(uppercaseRequest)
  }
}

def timeoutFilter[Req, Rep](duration: Duration) = {
  val exc = new IndividualRequestTimeoutException(duration)
  val timer = DefaultTimer.twitter
  new TimeoutFilter[Req, Rep](duration, exc, timer)
}

val filteredLog = timeoutFilter(2.seconds) andThen
  uppercaseFilter andThen
  clientServiceIface.log

val result: Future[Log.Result] = filteredLog(Log.Args("hello", 2))

Sometimes different Thrift methods have different latencies; for example the log method typically completes in two second, and the getLogSize method completes in 500 milliseconds. Then we can use a shorter timeout duration for getLogSize:

val filteredGetLogSize = timeoutFilter(500.milliseconds) andThen
  clientServiceIface.getLogSize

val size: Future[GetLogSize.Result] = filteredGetLogSize(GetLogSize.Args())

Backwards compatibility

Existing code works without changes. We don’t plan to deprecate or remove the method-based interfaces.

Additionally, there is a bridge constructor to wrap the old-style method interface around the new service interface.

val filteredMethodIface: LoggerService[Future] =
  Thrift.newMethodIface(clientServiceIface.copy(log = filteredLog))

Await.result(filteredMethodIface.log("ping", 3).map(println))

This can be used to insert filters into existing Thrift clients.

Please contact the Finaglers mailing list with questions or bug reports, and if you’re interested in working on projects like Scrooge, get in touch—the Core System Libraries team at Twitter is hiring.