Rest.li is an open source REST framework that uses asynchronous and non-blocking I/O to build scalable REST architectures. It achieves asynchronous behavior through its components R2 (network communication), D2 (dynamic discovery and load balancing), and by not blocking on I/O. Rest.li provides both callback-based and ParSeq-based approaches for building asynchronous Rest.li servers and clients to avoid blocking and improve throughput. ParSeq allows expressing complex asynchronous workflows through tasks and promises.
3. What is this Rest.li thing
you speak about?
“Rest.li is an open source REST framework for building robust,
scalable RESTful architectures using type-safe bindings and
asynchronous, non-blocking I/O.”
3
4. The Stack
Rest.li Data and REST API
D2 Dynamic Discovery and load balancing
R2 Network communication
4
9. How is R2 async and
non-blocking?
• Netty based async client
• Jetty based server – configuration change (link) needed to run
the server in async mode
Rest.li
D2
R2
9
10. How is D2 async and
non-blocking?
• All communication with ZooKeeper uses the asynchronous
APIs
• ZooKeeper pushes data to clients in case of updates
Rest.li
D2
R2
10
11. How is Rest.li async
and non-blocking?
• Does not handle I/O
• I/O is taken care of by R2
• R2 is async and non-blocking!
• Uses ParSeq when interacting with and delegating to
application code.
Rest.li
D2
R2
11
13. Async Server
Implementations
• 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)
13
15. Async Server – Callback
@RestMethod.Get
public void get(final Long id, @CallbackParam final Callback<Greeting>
callback) {
String path = "/data/" + id;
_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);
} 15
16. Async Server –
Callback Signature
@RestMethod.Get
public void get(final Long id, @CallbackParam
final Callback<Greeting> callback)
com.linkedin.common.Callback
16
17. Async Server – Filling
out the callback
_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); 17
18. Async Server – Callback
@RestMethod.Get
public void get(final Long id, @CallbackParam final Callback<Greeting>
callback) {
String path = "/data/" + id;
_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);
} 18
20. Key ParSeq Concepts
• Promise Fully async Java Future + listener mechanism.
• Task Basic unit of work that is similar to Java Callable.
Task<T>
• Engine runs Tasks.
• Context Used by a Task to run sub-Tasks.
• Composition par and seq
(link)
20
21. Async Server – 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);
}
21
22. Async Server – ParSeq
signature
@RestMethod.Get
public Task<Greeting> get(final Long id)
22
23. Async Server – ParSeq body
final Task<FileData> fileDataTask = buildFileDataTask();
23
24. Async Server – ParSeq body
final Task<Greeting> mainTask = Tasks.callable("main", new
Callable<Greeting>() {
@Override
public Greeting call() throws Exception {
FileData fileData = fileDataTask.get();
return buildGreetingFromFileData(id, fileData);
}
});
24
25. Async Server –
assembling the Tasks
return Tasks.seq(fileDataTask, mainTask);
25
fileDataTask mainTask
26. Async Server – 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);
}
26
27. Async Server – ParSeq
• Can use this approach to build complex call graphs and
return the final Task. (more on this later).
• Can use ParSeq tracing
• No callback hell!
27
29. Async Rest.li requests
• RestClient is async and non-blocking under the hood – it
uses ParSeq and Netty.
• Applications can still block the thread if they block on the
results of the Rest.li call!
Response response =
_restClient.sendRequest(BUILDER.get().id(1L).build()).get();
Fortune fortune = response.getEntity();
Blocking on the result of a Future
29
30. Async Rest.li requests
Two Options:
1. Callback based API
2. Use a ParSeqRestClient – This is a wrapper around a
RestClient that returns ParSeq Tasks and Promises.
30
31. Async Rest.li requests
– Callbacks
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);
31
32. Async Rest.li requests – defining
a Callback
Callback<Response<Greeting>> callback = new
Callback() {
void onSuccess(Response<Greeting> response) {
// do something with the returned Greeting
}
void onError(Throwable e) {
// whoops
}
}
32
33. Async Rest.li requests – sending
the request
Request<Greeting> getRequest =
BUILDERS.get().id(1L).build();
_restClient.sendRequest(getRequest,
new RequestContext(),
callback);
33
34. Async Rest.li requests – using
ParSeq
• Callback based API is good for simple use cases (one or two
requests and you are done.)
• But it is hard to express more complicated call flows using the
Callback API
Callback ParSeq
34
40. Conclusion
• Rest.li is async and non-blocking under the hood
• You can use Callbacks and ParSeq Tasks and Promises to
write async Rest.li clients and servers
40
41. Links and references
• Rest.li user guide https://github.com/linkedin/rest.li/wiki/Rest.li-
User-Guide
• Asynchronous servers and clients in Rest.li
https://github.com/linkedin/rest.li/wiki/Asynchronous-Servers-and-
Clients-in-Rest.li
• ParSeq https://github.com/linkedin/parseq
41
R2 – RESTful abstraction built on Netty and Jetty.
D2 – Dynamic discovery and client side load balancing using ZooKeeper.
Rest.li – Data layer + layer that exposes RESTful API that application can use to build RESTful services.
Async I/O basically means that we free up our thread of computation to do other things while we are waiting for the I/O to complete. In other words we make an I/O request and we don’t block waiting on the result of the I/O to appear.
Let’s look at one thread on the server.
Assume both request 1 and request 2 arrive at the same time.
In each request the thread does some work, then makes an I/O request of some sort, and then uses the values from the I/O response to fulfill the request.
In the synchronous model we can’t start processing request 2 until request 1 is done because the thread is blocked on I/O. In the gray portions our server is essentially idle.
In the async model we can start processing request 2 instead of blocking on the I/O to complete.
For the rest of the talk we will assume we are running the R2 server in async mode.
Jetty configuration is needed to run Rest.li in async mode.
If you are running Rest.li in a thread-per-request model async programming will not make much of a difference since the thread cannot be re-used to serve new requests.
ParSeq is a framework that makes it easier to write asynchronous code in Java. Created by LinkedIn and is an open source project.
In this example we are fetching data from ZooKeeper using the async ZooKeeper API and then converting that data into a Greeting object to return.
Need to explicitly specify the callback in the method signature.
Notice the void return type. The next slide will explain how a response is sent back to the client.
The callback API exposes two methods – onSuccess and onError.
In this example we invoke the onSuccess method if the data we get from ZooKeeper has a length > 0.
We invoke the onError method otherwise.
Once either onError or onSucess has been invoked the framework will return a response back to the client.
ParSeq is a library that makes it easy to write complex async task flows in Java. It was created by LinkedIn and is an open source project.
Difference between Task and Callable is that in a Task you can set the result asynchronously.
In this example we read data from the disk asynchronously and use that to build a Greeting object.
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.
Return ParSeq tasks from methods. These are run by Rest.li using the internal ParSeq engine.
Task<T> if you run this Task using a ParSeq engine it will return you a T eventually.
We first read a file on disk using some async I/O abstraction that gives us back a Task for the data read.
The basic idea to use ParSeq for Rest.li async method implementations is to return Tasks or Promises.
We want to transform the 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.
This is what will be returned from the overall method.
Tasks.seq is a way to build a call graph between Tasks in ParSeq. In this example we are saying that we want mainTask to run after fileDataTask. This is because mainTask depends on fileDataTask. Once we express our call graph dependency in this way, when we call fileDataTask.get() within the mainTask it will either return right away or throw an exception. This is because ParSeq will make sure that mainTask only gets run AFTER fileDataTask has completed.
Tasks.seq() returns a new Task and this Task will get run by the underlying ParSeq engine within Rest.li.
This call is blocking because sendRequest returns a Future, and calling get() on the Future waits until the operation has been completed.
This is very similar to callback based approached present in Node.js and JavaScript.
The first thing we need to do is define the callback that will be executed when the server sends us a response, or there is an error.
Then we use the Callback based sendRequest API to send the request. Rest.li invokes the callback for you upon getting a result.
Each circle is a response from making a Rest.li request. Each layer represents requests being made in parallel. The yellow circle Is the final result.
b1 and b2 don’t depend on each other so theoretically b2 could have been run in the same level as a1, a2, or a3.
Use the parseq rest client to create three response tasks.
Combine the results from a1-a3 and use that to make another request task b1
Similarly combine the results of b1 and b2 and create another request task c1
Use the Tasks.seq() and Tasks.par() methods to build the call graph. Within each level we want each Task to run in parallel, which is why we use Tasks.par(a1ResponseTask, a2ResponseTask, a3ResponseTask) and Tasks.par(b1ResponseTask, b2ResponseTask). Finally, we want each level to run sequentially one after the other, which is why wrap everything in a Tasks.seq.