Skip to content

Asynchronous Servers and Clients in Rest.li

karanparikh edited this page Nov 15, 2014 · 38 revisions

Rest.li is asynchronous and non-blocking under the hood -

  • R2: On the client side R2 uses a Netty based asynchronous client. On the server side if you are using our experimental Netty server is async and non-blocking. If you are using Jetty you need to configure it to run in async mode.
  • D2: All communication with ZooKeeper uses the async APIs.
  • Rest.li: Rest.li does not handle I/O. All I/O work is done by R2, which is async and non-blocking as explained above. Rest.li uses ParSeq to interact with and delegate to server application code. The RestClient used to make Rest.li requests on the client-side has several options in order to write async non-blocking code.

Async Server Implementations

As shown above the Rest.li framework is async and non-blocking under the hood.

As a result of this, if you do any blocking work in your method implementation it can negatively impact your application throughput as threads are held up by your application which are needed by Rest.li. (if you are using Rest.li in async mode)

There are two main options available to write async server implementations in Rest.li -

  1. Using com.linkedin.common.Callbacks (included in Rest.li)
  2. Using ParSeq

Using Callbacks

Consider the following implementation of a async GET method using Callbacks. In this example we fetch data from ZooKeeper asynchronously, and based on the data that we get back either return a 404 to the user, or build a Greeting RecordTemplate. We will use this example to understand how to write Callback based Rest.li async method implementations -

@RestMethod.Get
public void get(final Long id, @CallbackParam final Callback<Greeting> callback) {
  String path = "/data/" + id;
  // _zkClient is a regular ZooKeeper client
  _zkClient.getData(path, false, new DataCallback() {
    public void processResult(int i, String s, Object o, byte[] b, Stat st) {
      if (b.length == 0) {
        callback.onError(new RestLiServiceException(HttpStatus.S_404_NOT_FOUND));
      }
      else {
        callback.onSuccess(buildGreeting(b));
      }
    }
  }, null);
}

Signature

@RestMethod.Get
public void get(final Long id, @CallbackParam final Callback<Greeting> callback) {

In order to use Callbacks we need to set the return type of the function to be void and pass in a Callback<T> as a parameter to the function. T here is whatever type you would have returned from a synchronous implementation of the same function. In this case the synchronous implementation would have returned a Greeting, which is why we are returning a Callback<Greeting> here.

Function body

_zkClient.getData(path, false, new DataCallback() {
  public void processResult(int i, String s, Object o, byte[] b, Stat st) {
    if (b.length == 0) {
      callback.onError(new RestLiServiceException(HttpStatus.S_404_NOT_FOUND));
    }
    else {
      callback.onSuccess(buildGreeting(b));
    }
  }
}, null);

We use the async ZooKeeper getData API call to fetch data from ZooKeeper. Based on the data we get back from ZooKeeper in the DataCallback (which is a ZooKeeper construct) we invoke either the onError or the onSuccess method on the Callback interface.

onError is used to signify that something went wrong. In this case we invoke onError with a RestliServiceException when the length of data that we get back from ZooKeeper is 0. The Rest.li framework translates this Exception into an appropriate REST response to send back to the client.

In case we get back data that has non-zero length we build a Greeting object from it in the buildGreeting method (not shown here) and return that to the client by invoking the onSuccess method.

In other words, all you have to do is invoke the onError or onSuccess method within your method and the Rest.li framework will return data back to the client. This is why the return type for the method is void, since the Callback is used to return values back to the client.

Callback execution

It is up to the application developer to execute the Callback. Rest.li does not execute the Callback for you. In the above example, the Callback was executed by the ZooKeeper thread, which is why we didn't have to explicitly execute it.

Using ParSeq

Consider the following example of an async GET implementation that uses ParSeq -

@RestMethod.Get
public Task<Greeting> get(final Long id) {
  final Task<FileData> fileDataTask = buildFileDataTask();
  final Task<Greeting> mainTask = Tasks.callable("main", new Callable<Greeting>() {
    @Override
    public Greeting call() throws Exception {
      FileData fileData = fileDataTask.get();
      return buildGreetingFromFileData(id, fileData);
    }
  });
  return Tasks.seq(fileDataTask, mainTask);
}

buildFileDataTask (implementation not shown here) reads some file on disk using async I/O and returns a Task<FileData>, where FileData (implementation not shown here) is some abstraction for the data being read. We use FileData to build a Greeting to return to the client.

Signature

@RestMethod.Get
public Task<Greeting> get(final Long id) {

In order to use ParSeq the function implementation must return either a ParSeq Task<T> or a Promise<T>. T here is whatever type you would have returned from a synchronous implementation of the same function. In this case the synchronous implementation would have returned a Greeting, which is why we are returning a Task<Greeting> here.

Function Body

final Task<FileData> fileDataTask = buildFileDataTask();
final Task<Greeting> mainTask = Tasks.callable("main", new Callable<Greeting>() {
  @Override
  public Greeting call() throws Exception {
    FileData fileData = fileDataTask.get();
    return buildGreetingFromFileData(id, fileData);
  }
});
return Tasks.seq(fileDataTask, mainTask);

The basic idea to use ParSeq for Rest.li async method implementations is to return Tasks or Promises.

fileDataTask is a Task for the FileData that we read from disk. We want to transform this FileData into a Greeting to return to the user. We define a new Task, called mainTask in the example above, to do this.

Within the call method of mainTask we obtain the FileData from the fileDataTask. This is a non-blocking call because of the way we assemble our final call graph (more on this in a bit). Finally, we build a Greeting in the buildGreetingFromFileData (implementation not shown here) method.

So we have two Tasks now, fileDataTask and mainTask, with mainTask depending on the result of fileDataTask. mainTask also builds the Greeting object that we want to return to the client. In order to build this dependency between the two Tasks we use the Tasks.seq method.

Task execution

In the above example Task.seq(fileDataTask, mainTask) returns a new Task that is executed for you automatically using the ParSeq engine within Rest.li. In other words, you do not have to provide a separate ParSeq execution engine to run this Task. Rest.li runs the Task for you and returns an appropriate response to the client.

Async Client Implementations

There are two main options available to make async requests using Rest.li -

  1. Using com.linkedin.common.Callbacks
  2. Using a ParSeqRestClient

Using Callbacks

Here is a partial example of making a GET request to the /greetings resource and then using the result asynchronously -

Callback<Response<Greeting>> cb = new Callback() {
  void onSuccess(Response<Greeting> response) {
    // do something with the returned Greeting
  }
  void onError(Throwable e) {
    // whoops
  }
}

Request<Greeting> getRequest = BUILDERS.get().id(1L).build();

_restClient.sendRequest(getRequest, new RequestContext(), cb);

Defining the Callback

Callback<Response<Greeting>> cb = new Callback() {
  void onSuccess(Response<Greeting> response) {
    // do something with the returned Greeting
  }
  void onError(Throwable e) {
    // whoops
  }
}

We need to define the Callback that will be executed when we get back a Response from the server. onSuccess will be invoked by Rest.li on getting a non-exception result (i.e. a Response), while onError will be invoked by Rest.li in case an Exception is thrown. The key concept to note here is that Rest.li invokes the Callback from you once we get a Response or an Exception is thrown.

Sending the Request

Request<Greeting> getRequest = BUILDERS.get().id(1L).build();
_restClient.sendRequest(getRequest, new RequestContext(), callback);

We pass the Callback we defined previously as the last parameter of the sendRequest call. This calls returns right away, because as stated above, Rest.li invokes the Callback for you appropriately.

Using a ParSeqRestClient

Clone this wiki locally