Communicate with a Thrift Service

Finatra provides support for integrating with Scrooge-generated Finagle Thrift clients.

As mentioned in the Scrooge Finagle Integration documentation, users have three API choices for building an interface to a Finagle Thrift client — ServicePerEndpoint, ReqRepServicePerEndpoint, and MethodPerEndpoint.

The MethodPerEndpoint interface is a collection of methods which return Futures.

The ServicePerEndpoint interface is a collection of Finagle Services where each Thrift method is represented as a Finagle Service of ThriftMethod.Args to ThriftMethod.SuccessType:

Service[method.Args, method.SuccessType]

By their nature of being Services, the methods are composable with Finagle’s Filters.

The ReqRepServicePerEndpoint interface is also a collection of Finagle Services, however methods are Services from a c.t.scrooge.Request to a c.t.scrooge.Response, e.g.,

import com.twitter.scrooge.{Request, Response}

Service[Request[method.Args], Response[method.SuccessType]]

These envelope types allow for the passing of header information between clients and servers when using the Finagle ThriftMux protocol.

Getting Started

To start, add a dependency on the Finatra inject-thrift-client library.

E.g., with sbt:

"com.twitter" %% "inject-thrift-client" % "24.2.0",

ThriftMethodBuilderClientModule

The c.t.inject.thrift.modules.ThriftMethodBuilderClientModule allows for configuration of a Finagle Thrift client using the Finagle ThriftMux MethodBuilder integration.

Users have the option to configure a service-per-endpoint, i.e., ServicePerEndpoint or ReqRepServicePerEndpoint interface of the Thrift client and the c.t.inject.thrift.modules.ThriftMethodBuilderClientModule provides bindings for both the chosen service-per-endpoint interface and the MethodPerEndpoint interface. In this case, the MethodPerEndpoint interface functions as a thin wrapper over the configured service-per-endpoint.

The choice to interact with Finagle Services when calling the configured Thrift client or to use the Thrift method interface is up to you. You will have access to both in the object graph when implementing a ThriftMethodBuilderClientModule.

To create a new client, first create a new TwitterModule which extends c.t.inject.thrift.modules.ThriftMethodBuilderClientModule:

import com.twitter.finagle.service.RetryBudget
import com.twitter.inject.thrift.modules.ThriftMethodBuilderClientModule
import com.twitter.myservice.thriftscala.MyService
import com.twitter.util.{Duration, Monitor}

object MyServiceModule
  extends ThriftMethodBuilderClientModule[
    MyService.ServicePerEndpoint,
    MyService.MethodPerEndpoint] {

  override val dest: String = "flag!myservice-thrift-service"
  override val label: String = "myservice-thrift-client"

  override val sessionAcquisitionTimeout: Duration = ???
  override val requestTimeout: Duration = ???
  override val retryBudget: RetryBudget = ???
  override val monitor: Monitor = ???
}

The ThriftMethodBuilderClientModule implementation must be typed to a Scrooge-generated service-per-endpoint and the MethodPerEndpoint. Users can choose either the MyService.ServicePerEndpoint interface (as in the above example) or the MyService.ReqRepServicePerEndpoint interface depending on their requirements:

object MyServiceModule
  extends ThriftMethodBuilderClientModule[
    MyService.ReqRepServicePerEndpoint,
    MyService.MethodPerEndpoint] {
  ...

At a minimum, to use the c.t.inject.thrift.modules.ThriftMethodBuilderClientModule, a ThriftMux client label and a String dest must be specified.

Configuration

The ThriftMethodBuilderClientModule intends to allow users to configure ThriftMux client semantics and apply filters per-method via the c.t.inject.thrift.ThriftMethodBuilder which is a thin wrapper over the Finagle ThriftMux MethodBuilder.

Advanced ThriftMux client configuration can be done by overriding the ThriftMethodBuilderClientModule#configureThriftMuxClient method which allows for ad-hoc ThriftMux client configuration.

See Finagle Client Modules for more information on client configuration parameters and their meanings.

Per-Method Configuration

To configure per-method semantics, override and provide an implementation for the ThriftMethodBuilderClientModule#configureServicePerEndpoint method. E.g.,

import com.twitter.inject.Injector
import com.twitter.inject.thrift.ThriftMethodBuilderFactory
import com.twitter.inject.thrift.modules.ThriftMethodBuilderClientModule
import com.twitter.myservice.thriftscala.MyService

object MyServiceModule
  extends ThriftMethodBuilderClientModule[
    MyService.ServicePerEndpoint,
    MyService.MethodPerEndpoint] {

  override val dest: String = "flag!myservice-thrift-service"
  override val label: String = "myservice-thrift-client"

  override protected def configureServicePerEndpoint(
    injector: Injector,
    builder: ThriftMethodBuilderFactory[MyService.ServicePerEndpoint],
    servicePerEndpoint: MyService.ServicePerEndpoint
  ): MyService.ServicePerEndpoint = {

    servicePerEndpoint
      .withFoo(
        builder.method(MyService.Foo)
          .withTimeoutPerRequest(???)
          .withTotalTimeout(???)
          .withRetryForClassifier(???)
          .filtered(new MyFooMethodFilter)
          .service)
      .withBar(
        builder.method(MyService.Bar)
          .filtered(new MyTypeAgnosticFilter)
          .withRetryForClassifier(???)
          .service)
  }
}

In this example we are configuring the given servicePerEndpoint by re-implementing the Foo and Bar functions using a “builder”-like API. Each Scrooge-generated client-side ServicePerEndpoint provides a withXXXX function over every defined Thrift method that allows users to replace the current implementation of the method with a new implementation. The replacement must still be a correctly-typed Finagle Service.

In the above example we replace the methods with implementations built up from a combination of MethodBuilder functionality and arbitrary filters ending with a call to ThriftMethodBuilder#service which materializes the resultant Service[-Req, +Rep].

Global Filters

Note that TypeAgnostic Finagle Filters can also be applied “globally” across the all methods of a ServicePerEndpoint interface by calling ServicePerEndpoint#filtered.

For example, to apply a set of TypeAgnostic Finagle Filters to a ServicePerEndpoint:

import com.twitter.inject.Injector
import com.twitter.inject.thrift.ThriftMethodBuilderFactory
import com.twitter.inject.thrift.modules.ThriftMethodBuilderClientModule
import com.twitter.myservice.thriftscala.MyService

object MyServiceModule
  extends ThriftMethodBuilderClientModule[MyService.ServicePerEndpoint, MyService.MethodPerEndpoint] {

  override val dest: String = "flag!myservice-thrift-service"
  override val label: String = "myservice-thrift-client"

  override protected def configureServicePerEndpoint(
    injector: Injector,
    builder: ThriftMethodBuilderFactory[MyService.ServicePerEndpoint],
    servicePerEndpoint: MyService.ServicePerEndpoint
  ): MyService.ServicePerEndpoint = {

    servicePerEndpoint
      .filtered(???)
  }
}

This can be combined with the per-method configuration as well.

ThriftMethodBuilderClientModule Bindings

When included in a server’s module list, an implementation of the ThriftMethodBuilderClientModule will provide bindings to both MyService.ServicePerEndpoint (or MyService.ReqRepServicePerEndpoint) and MyService.MethodPerEndpoint.

For example, given the following ThriftMethodBuilderClientModule implementation:

import com.twitter.inject.thrift.modules.ThriftMethodBuilderClientModule
import com.twitter.myservice.thriftscala.MyService

object MyServiceModule
  extends ThriftMethodBuilderClientModule[
    MyService.ServicePerEndpoint,
    MyService.MethodPerEndpoint] {

  override val dest: String = "flag!myservice-thrift-service"
  override val label: String = "myservice-thrift-client"
}

This means that both the MyService.ServicePerEndpoint and MyService.MethodPerEndpoint types will be injectable. Which to use is dependent on your use-case.

import com.twitter.finagle.http.Request
import com.twitter.finatra.http.Controller
import com.twitter.myservice.thriftscala.MyService
import javax.inject.{Inject, Singleton}

@Singleton
class MyDataController @Inject()(
  myService: MyService.MethodPerEndpoint
) extends Controller {
  get("/") { request: Request =>
    myService.foo(request.params("data"))
  }
}

ThriftClientModule

Note

The c.t.inject.thrift.modules.ThriftClientModule only supports the ThriftMux protocol.

The c.t.inject.thrift.modules.ThriftClientModule allows for simpler configuration of a Finagle Thrift client than the c.t.inject.thrift.modules.ThriftMethodBuilderClientModule.

Users have the option of configuring either a MethodPerEndpoint or the higher-kinded, e.g., MyService[+MM[_]], interface of the Thrift client and the c.t.inject.thrift.modules.ThriftClientModule provides a binding to the chosen interface.

To create a new client, first create a new TwitterModule which extends c.t.inject.thrift.modules.ThriftClientModule:

import com.twitter.inject.thrift.modules.ThriftClientModule
import com.twitter.myservice.thriftscala.MyService

object MyServiceModule
  extends ThriftClientModule[MyService.MethodPerEndpoint] {

  override val dest: String = "flag!myservice-thrift-service"
  override val label: String = "myservice-thrift-client"
}

The ThriftClientModule implementation must be typed to either the Scrooge-generated MethodPerEndpoint or the MyService[+MM[_]] Thrift service interface. These interfaces are semantically equivalent, however there are differences when it comes to some testing features which have trouble dealing with higher-kinded types (like mocking).

Users can choose either interface depending on their requirements. E.g., to use the MyService[+MM[_]] interface for MyService:

object MyServiceModule
  extends ThriftClientModule[MyService.MethodPerEndpoint] {
  ...

At a minimum, to use the c.t.inject.thrift.modules.ThriftClientModule, a ThriftMux client label and a String dest must be specified.

Configuration

The ThriftClientModule intends to allow users to easily configure common parameters of a ThriftMux client.

import com.twitter.finagle.service.RetryBudget
import com.twitter.inject.Injector
import com.twitter.inject.thrift.ThriftMethodBuilderFactory
import com.twitter.inject.thrift.modules.ThriftClientModule
import com.twitter.myservice.thriftscala.MyService
import com.twitter.util.{Duration, Monitor}

object MyServiceModule
  extends ThriftClientModule[MyService.MethodPerEndpoint] {

  override val dest: String = "flag!myservice-thrift-service"
  override val label: String = "myservice-thrift-client"

  override val sessionAcquisitionTimeout: Duration = ???

  override val requestTimeout: Duration = ???

  override val retryBudget: RetryBudget = ???

  override val monitor: Monitor = ???

Advanced ThriftMux configuration can be done by overriding the ThriftClientModule#configureThriftMuxClient method which allows for ad-hoc ThriftMux client configuration.

See Finagle Client Modules for more information on client configuration parameters and their meanings.

ThriftClientModule Bindings

When included in a server’s module list, an implementation of the ThriftClientModule will provide a binding of the specified type param to the object graph. Either MyService.MethodPerEndpoint or MyService.MethodPerEndpoint.

For example, given the following ThriftClientModule implementation:

import com.twitter.inject.thrift.modules.ThriftClientModule
import com.twitter.myservice.thriftscala.MyService

object MyServiceModule
  extends ThriftClientModule[MyService.MethodPerEndpoint] {

  override val dest: String = "flag!myservice-thrift-service"
  override val label: String = "myservice-thrift-client"
}

This means that the MyService.MethodPerEndpoint type will be injectable.

import com.twitter.finagle.http.Request
import com.twitter.finatra.http.Controller
import com.twitter.myservice.thriftscala.MyService
import javax.inject.{Inject, Singleton}

@Singleton
class MyDataController @Inject()(
  myService: MyService.MethodPerEndpoint
) extends Controller {
  get("/") { request: Request =>
    myService.foo(request.params("data"))
  }
}

More Information

More Information on Modules:

Module best practices and depending on other modules.

For more information on Scrooge-generated client interfaces see the Finagle Integration section of the Scrooge documentation.

More detailed examples are available in the integration tests:

which test over multiple implementations of a ThriftClientModule and ThriftMethodBuilderClientModule: