Drift Client is a simple library for creating Thrift clients using annotations.
In Drift, a Thrift client is simply a Java interface annotated with @ThriftService
. In
addition to annotating the interface directly, Drift supports annotations on super interfaces.
For example, the following describes a Scribe client:
@ThriftService
public interface Scribe
{
@ThriftMethod
ResultCode log(List<LogEntry> messages);
}
To make the client method asynchronous, simply change the return type of the
corresponding method to a ListenableFuture
, as shown here:
@ThriftService
public interface Scribe
{
@ThriftMethod()
ListenableFuture<List<Integer>> getValues();
}
The future will be completed when the server returns a value. When adding listeners that do non-trivial work when the future is completed, keep in mind that if you do not provide an executor for listeners to run on, they will run on the NIO threads, and therefore can potentially block other clients.
In Thrift, method parameters are encoded as a Thrift struct, so each parameter
must have a name and an ID. If you do not specify an ID, Drift will assume the
parameters are numbered starting with 1
. If you do not specify a name, Drift
will attempt to determine the names automatically. For this to work, the code
must be compiled with parameter names enabled (pass the -parameters
option to javac
).
If you want to use a different ID or name, simply annotate the parameter as follows:
@ThriftService
public interface Scribe
{
@ThriftMethod
ResultCode log(@ThriftField(value = 3, name = "mesg") List<LogEntry> messages);
}
As with method parameters, Thrift encodes the response as a struct with field
zero being a standard return and exceptions be stored in higher number fields.
If the Java method throws only one exception annotated with @ThriftStruct
,
Drift will assume the result struct field id is 1
. Otherwise, you will need to
add the @ThriftId
annotation to the exception declaration:
@ThriftMethod
void doSomething() throws @ThriftId(1) MyException, @ThriftId(2) MyOther;
For asynchronous methods, which do not directly throw exceptions, you will need
to use the @ThriftException
annotation:
@ThriftMethod(exception = {
@ThriftException(type = MyException.class, id = 1),
@ThriftException(type = MyOther.class, id = 2),
})
ListenableFuture<Void> doSomething();
A Drift client is thread safe and concurrent, so it can be used by multiple threads at the same time. The user of the client simply calls methods on the interface and Drift manages address selection and connection pooling. For example:
// create client factory (only create this expensive object once)
DriftClientFactory clientFactory = // see below
// create a client (also only create this once)
Scribe scribe = clientFactory.createDriftClient(Scribe.class);
// use client
scribe.log(ImmutableList.of(new LogEntry("category", "message")));
A Drift client can either be created manually using a static factory or injected using Guice.
The following code manually constructs a DriftClientFactory
using the Netty transport:
// server address
List<HostAndPort> addresses = ImmutableList.of(HostAndPort.fromParts("localhost", 1234));
// expensive services that should only be created once
ThriftCodecManager codecManager = new ThriftCodecManager();
AddressSelector addressSelector = new SimpleAddressSelector(addresses);
DriftNettyClientConfig config = new DriftNettyClientConfig();
// methodInvokerFactory must be closed
DriftNettyMethodInvokerFactory<?> methodInvokerFactory = DriftNettyMethodInvokerFactory
.createStaticDriftNettyMethodInvokerFactory(config);
// client factory
DriftClientFactory clientFactory = new DriftClientFactory(codecManager, methodInvokerFactory, addressSelector);
As you can see, the construction of a DriftClientFactory
requires a few supporting
services, which are described below.
A ThriftCodecManager
caches the description of every Thrift type used by the clients. Extracting
service metadata is expensive, so a single ThriftCodecManager
should be used to create
all the clients you will need.
An AddressSelector
selects host addresses for the client to connect to. Typically,
this service tracks all hosts running the service and selects a random subset to try
for an invocation. The AddressSelector
is also notified of addresses that failed to
connect, allowing it to perform simple tracking of host state.
A MethodInvokerFactory
handles connection and worker thread pools. Connections and
threads are expensive resources, so a single MethodInvokerFactory
should be used to create
all the clients you will need. Since the MethodInvokerFactory
is maintaining pools, a close
method is typically provided to shutdown the pools.
There are currently two transport implementations of MethodInvokerFactory
. Drift Netty, which
is used in the example above, provides DriftNettyMethodInvokerFactory
, and Apache Thrift provides
ApacheThriftMethodInvokerFactory
. Each transport requires sightly different configuration,
so each transport provides a different configuration class.
Drift includes optional support for binding clients into Guice.
To bind a client, add a transport implementation module (e.g., DriftNettyClientModule
or
ApacheThriftClientModule
), and bind the clients with the fluent DriftClientBinder
.
The following binds a Scribe
client that will connect to example.com:1234
by default:
// server address
List<HostAndPort> addresses = ImmutableList.of(HostAndPort.fromParts("localhost", 1234));
// see io.airlift.bootstrap.Bootstrap for a simpler system to create Guice services with configuration
Injector injector = Guice.createInjector(Stage.PRODUCTION,
new ConfigurationModule(new ConfigurationFactory(ImmutableMap.of())),
new DriftNettyClientModule(),
binder -> driftClientBinder(binder).bindDriftClient(Scribe.class)
.withAddressSelector(simpleAddressSelector(addresses)));
Then, Guice can inject a Thrift client implementation:
@Inject
public MyClass(Scribe scribeClient)
{
scribeClient.log(entries);
}