Rest.liDevelopment Workflow Overview
Joe Betz, April 2014
Let’s implement a Simple REST request
Let’s implement a Simple REST request
GET /fortunes/1 HTTP/1.1 Accept: application/json
Request
Let’s implement a Simple REST request
GET /fortunes/1 HTTP/1.1 Accept: application/json
HTTP/1.1 200 OK Content-Type: application/json Content-Length: … !{ "message": "Today’s your lucky day!" }
Request Response
Step 1. Write a Data Schema
Step 1. Write a Data SchemaFortune.pdsc { "name" : "Fortune", "namespace" : "com.example", "type" : "record", "fields" : [ { "name" : "message", "type" : "string" } ] }
Step 1. Write a Data SchemaFortune.pdsc { "name" : "Fortune", "namespace" : "com.example", "type" : "record", "fields" : [ { "name" : "message", "type" : "string" } ] }
This schema defines a Fortune record. It has a single message field of type string. Rest.li schemas are based on avro schemas, and supports maps, lists, optional fields, unions and other useful data modeling constructs.
Step 1. Write a Data SchemaFortune.pdsc { "name" : "Fortune", "namespace" : "com.example", "type" : "record", "fields" : [ { "name" : "message", "type" : "string" } ] }
Rest.li schemas are designed for JSON. Here’s what a Fortune looks like in JSON: { “message”: “Today is your lucky day!” }
Step 1. Write a Data SchemaFortune.pdsc { "name" : "Fortune", "namespace" : "com.example", "type" : "record", "fields" : [ { "name" : "message", "type" : "string" } ] }
Rest.li’s automatically generates a binding class from our data schema:
Step 1. Write a Data SchemaFortune.pdsc { "name" : "Fortune", "namespace" : "com.example", "type" : "record", "fields" : [ { "name" : "message", "type" : "string" } ] }
Fortune.java public class Fortune extends RecordTemplate { String getMessage(); void setMessage(String); }
Rest.li’s automatically generates a binding class from our data schema:
Generated
Code
Step 1. Write a Data SchemaFortune.pdsc { "name" : "Fortune", "namespace" : "com.example", "type" : "record", "fields" : [ { "name" : "message", "type" : "string" } ] }
Fortune.java public class Fortune extends RecordTemplate { String getMessage(); void setMessage(String); }
Rest.li’s automatically generates a binding class from our data schema:
Generated
CodeThis class is important, we’ll use it in a minute.
Step 2. Write a REST Resource
FortunesResource.java @RestLiCollection(name = "fortunes") class FortunesResource implements KeyValueResource<Long, Fortune> { ! @RestMethod.GET public Fortune get(Long key) { return new Fortune() .setMessage("Today’s your lucky day!"); } }
Step 2. Write a REST Resource
FortunesResource.java @RestLiCollection(name = "fortunes") class FortunesResource implements KeyValueResource<Long, Fortune> { ! @RestMethod.GET public Fortune get(Long key) { return new Fortune() .setMessage("Today’s your lucky day!"); } }
Step 2. Write a REST ResourceRest.li annotation declares this class as our implementation of the /fortunes resource.
FortunesResource.java @RestLiCollection(name = "fortunes") class FortunesResource implements KeyValueResource<Long, Fortune> { ! @RestMethod.GET public Fortune get(Long key) { return new Fortune() .setMessage("Today’s your lucky day!"); } }
Step 2. Write a REST Resource
Our collection contains Fortune entities, keyed by Long.
FortunesResource.java @RestLiCollection(name = "fortunes") class FortunesResource implements KeyValueResource<Long, Fortune> { ! @RestMethod.GET public Fortune get(Long key) { return new Fortune() .setMessage("Today’s your lucky day!"); } }
Step 2. Write a REST Resource
Our collection contains Fortune entities, keyed by Long.Notice how we use the generated Fortune class here so everything is strongly typed.
FortunesResource.java @RestLiCollection(name = "fortunes") class FortunesResource implements KeyValueResource<Long, Fortune> { ! @RestMethod.GET public Fortune get(Long key) { return new Fortune() .setMessage("Today’s your lucky day!"); } }
Step 2. Write a REST Resource
Here we implement HTTP GET
FortunesResource.java @RestLiCollection(name = "fortunes") class FortunesResource implements KeyValueResource<Long, Fortune> { ! @RestMethod.GET public Fortune get(Long key) { return new Fortune() .setMessage("Today’s your lucky day!"); } }
Step 2. Write a REST Resource
From our resource implementation, Rest.li’s automatically generates an interface definition,
FortunesResource.java @RestLiCollection(name = "fortunes") class FortunesResource implements KeyValueResource<Long, Fortune> { ! @RestMethod.GET public Fortune get(Long key) { return new Fortune() .setMessage("Today’s your lucky day!"); } }
Step 2. Write a REST Resource
fortunes.restspec.json { “path”: “/fortunes”, “supports”: [ “get” ], … }
From our resource implementation, Rest.li’s automatically generates an interface definition,
Generated
Code
FortunesResource.java @RestLiCollection(name = "fortunes") class FortunesResource implements KeyValueResource<Long, Fortune> { ! @RestMethod.GET public Fortune get(Long key) { return new Fortune() .setMessage("Today’s your lucky day!"); } }
Step 2. Write a REST Resource
fortunes.restspec.json { “path”: “/fortunes”, “supports”: [ “get” ], … }
From our resource implementation, Rest.li’s automatically generates an interface definition, and client bindings.
Generated
Code
FortunesResource.java @RestLiCollection(name = "fortunes") class FortunesResource implements KeyValueResource<Long, Fortune> { ! @RestMethod.GET public Fortune get(Long key) { return new Fortune() .setMessage("Today’s your lucky day!"); } }
Step 2. Write a REST Resource
FortunesBuilders.java public class FortunesBuilders { GetRequestBuilder get(); }
fortunes.restspec.json { “path”: “/fortunes”, “supports”: [ “get” ], … }
From our resource implementation, Rest.li’s automatically generates an interface definition, and client bindings.
Generated
Code Generated
Code
FortunesResource.java @RestLiCollection(name = "fortunes") class FortunesResource implements KeyValueResource<Long, Fortune> { ! @RestMethod.GET public Fortune get(Long key) { return new Fortune() .setMessage("Today’s your lucky day!"); } }
Step 2. Write a REST Resource
FortunesBuilders.java public class FortunesBuilders { GetRequestBuilder get(); }
fortunes.restspec.json { “path”: “/fortunes”, “supports”: [ “get” ], … }
From our resource implementation, Rest.li’s automatically generates an interface definition, and client bindings.
Generated
Code Generated
Code
We’ll use these to build our client.
Step 3. Write a Client
Step 3. Write a ClientExampleClient.java Response response = restClient.sendRequest(new FortunesBuilders.get().id(1L)).get(); Fortune fortune = response.getEntity(); System.out.println(fortune.getMessage());
Step 3. Write a ClientExampleClient.java Response response = restClient.sendRequest(new FortunesBuilders.get().id(1L)).get(); Fortune fortune = response.getEntity(); System.out.println(fortune.getMessage());
We use client bindings generated from our /fortune resource to build an HTTP GET Request.
Step 3. Write a ClientExampleClient.java Response response = restClient.sendRequest(new FortunesBuilders.get().id(1L)).get(); Fortune fortune = response.getEntity(); System.out.println(fortune.getMessage());
We use client bindings generated from our /fortune resource to build an HTTP GET Request.
And we use the Rest.li RestClient to send the request.
Step 3. Write a ClientExampleClient.java Response response = restClient.sendRequest(new FortunesBuilders.get().id(1L)).get(); Fortune fortune = response.getEntity(); System.out.println(fortune.getMessage());
We use client bindings generated from our /fortune resource to build an HTTP GET Request.
And we use the Rest.li RestClient to send the request.
Notice how everything is strongly typed.
Step 3. Write a ClientExampleClient.java Response response = restClient.sendRequest(new FortunesBuilders.get().id(1L)).get(); Fortune fortune = response.getEntity(); System.out.println(fortune.getMessage());
We use client bindings generated from our /fortune resource to build an HTTP GET Request.
And we use the Rest.li RestClient to send the request.
Notice how everything is strongly typed.
But wait, this request is blocking! The .get() forces the thread block and wait for a response from the server. We’ll show how to make this non-blocking shortly.
Step 4. Run it!
Step 4. Run it!
“Today’s your lucky day!”
Step 4. Run it!
“Today’s your lucky day!”
Great! but all our code is synchronous and blocking. What about non-blocking?
Importance of async, non-blocking request handling
Importance of async, non-blocking request handlingAsync processing is ideal for making multiple requests to backend systems in parallel and then composing the results of those parallel requests into a single response. For modern internet architectures, where many backend systems are involved in handling each request from a consumer, making calls in parallel to these backend systems dramatically reduces the overall time to response back to the consumer.
Importance of async, non-blocking request handlingAsync processing is ideal for making multiple requests to backend systems in parallel and then composing the results of those parallel requests into a single response. For modern internet architectures, where many backend systems are involved in handling each request from a consumer, making calls in parallel to these backend systems dramatically reduces the overall time to response back to the consumer.
Servers running async code scale better because they can handle very large numbers of concurrent requests. This is because, when you write async code, no threads are blocked waiting for something to complete. If you don’t write async code and you need to handle large numbers of concurrent requests, you’ll need one thread per concurrent request. Each thread takes up considerable memory, and when you run out of memory (or max out a thread pool), the server is unable to take on more requests, resulting in timed out requests and in cases of many complex architectures, cascading failure.
Async in Rest.li
Async in Rest.liRest.li integrates with ParSeq for both client and server side async.
Async in Rest.liRest.li integrates with ParSeq for both client and server side async.
Using ParSeq, we will write Tasks. Tasks can be composed together in parallel (par) or sequence (seq) into larger tasks. Tasks are executed asynchronously by the ParSeq engine.
Async in Rest.liRest.li integrates with ParSeq for both client and server side async.
Using ParSeq, we will write Tasks. Tasks can be composed together in parallel (par) or sequence (seq) into larger tasks. Tasks are executed asynchronously by the ParSeq engine.
Let’s modify our Resource Implementation and Client to use ParSeq.
Async Resource Implementation
Async Resource ImplementationFortunesResource.java @RestLiCollection(name = "fortunes") class FortunesResource implements KeyValueResource<Integer, Fortune> { ! @RestMethod.GET public Task<Fortune> get(Long key) { Task<String> retrieveFortuneStr = … ; ! Task<Fortune> buildFortune = Tasks.action("getFortune", new Callable<Fortune>() { @Override public Fortune call() throws Exception { return new Fortune() .setMessage(retieveFortuneStr.get()); } }); return Tasks.seq(retrieveFortuneStr, buildFortune); } }
Async Resource ImplementationFortunesResource.java @RestLiCollection(name = "fortunes") class FortunesResource implements KeyValueResource<Integer, Fortune> { ! @RestMethod.GET public Task<Fortune> get(Long key) { Task<String> retrieveFortuneStr = … ; ! Task<Fortune> buildFortune = Tasks.action("getFortune", new Callable<Fortune>() { @Override public Fortune call() throws Exception { return new Fortune() .setMessage(retieveFortuneStr.get()); } }); return Tasks.seq(retrieveFortuneStr, buildFortune); } }
Rest.li resources methods can return a ParSeq Task. Rest.li will execute them asynchronously and respond with the result when it’s available.
Async Resource ImplementationFortunesResource.java @RestLiCollection(name = "fortunes") class FortunesResource implements KeyValueResource<Integer, Fortune> { ! @RestMethod.GET public Task<Fortune> get(Long key) { Task<String> retrieveFortuneStr = … ; ! Task<Fortune> buildFortune = Tasks.action("getFortune", new Callable<Fortune>() { @Override public Fortune call() throws Exception { return new Fortune() .setMessage(retieveFortuneStr.get()); } }); return Tasks.seq(retrieveFortuneStr, buildFortune); } }
Here we compose together two tasks together in sequence using Tasks.seq
Async Resource ImplementationFortunesResource.java @RestLiCollection(name = "fortunes") class FortunesResource implements KeyValueResource<Integer, Fortune> { ! @RestMethod.GET public Task<Fortune> get(Long key) { Task<String> retrieveFortuneStr = … ; ! Task<Fortune> buildFortune = Tasks.action("getFortune", new Callable<Fortune>() { @Override public Fortune call() throws Exception { return new Fortune() .setMessage(retieveFortuneStr.get()); } }); return Tasks.seq(retrieveFortuneStr, buildFortune); } }
Here we compose together two tasks together in sequence using Tasks.seq
The retrieveFortuneStr task is a non-blocking IO task to get a fortune string.
Async Resource ImplementationFortunesResource.java @RestLiCollection(name = "fortunes") class FortunesResource implements KeyValueResource<Integer, Fortune> { ! @RestMethod.GET public Task<Fortune> get(Long key) { Task<String> retrieveFortuneStr = … ; ! Task<Fortune> buildFortune = Tasks.action("getFortune", new Callable<Fortune>() { @Override public Fortune call() throws Exception { return new Fortune() .setMessage(retieveFortuneStr.get()); } }); return Tasks.seq(retrieveFortuneStr, buildFortune); } }
Here we compose together two tasks together in sequence using Tasks.seq
The retrieveFortuneStr task is a non-blocking IO task to get a fortune string.
The buildFortune task will create a Fortune from the result of the retrieveFortuneStr task. Notice how .get() is used to access the value of the completed retrieveFortuneStr.
Async ClientFortuneClient.java Task<Response<Fortune>> getFortune = parSeqClient.createTask(new FortunesBuilders.get().id(1L)); !final Task<Void> printFortune = Tasks.action("printFortune", new Runnable() { @Override public void run() { Response<Fortune> response = getFortune.get(); Fortune fortune = getResponseEntity(); System.out.println(fortune.getMessage()); } }); !engine.run(Tasks.seq(getFortune, printFortune)); !printFortune.await();
Async ClientFortuneClient.java Task<Response<Fortune>> getFortune = parSeqClient.createTask(new FortunesBuilders.get().id(1L)); !final Task<Void> printFortune = Tasks.action("printFortune", new Runnable() { @Override public void run() { Response<Fortune> response = getFortune.get(); Fortune fortune = getResponseEntity(); System.out.println(fortune.getMessage()); } }); !engine.run(Tasks.seq(getFortune, printFortune)); !printFortune.await();
We’ll use ParSeqRestClient, which can create Tasks from Rest.li requests.
Async ClientFortuneClient.java Task<Response<Fortune>> getFortune = parSeqClient.createTask(new FortunesBuilders.get().id(1L)); !final Task<Void> printFortune = Tasks.action("printFortune", new Runnable() { @Override public void run() { Response<Fortune> response = getFortune.get(); Fortune fortune = getResponseEntity(); System.out.println(fortune.getMessage()); } }); !engine.run(Tasks.seq(getFortune, printFortune)); !printFortune.await();
Here will compose together two tasks, also in sequence using Tasks.seq.
Async ClientFortuneClient.java Task<Response<Fortune>> getFortune = parSeqClient.createTask(new FortunesBuilders.get().id(1L)); !final Task<Void> printFortune = Tasks.action("printFortune", new Runnable() { @Override public void run() { Response<Fortune> response = getFortune.get(); Fortune fortune = getResponseEntity(); System.out.println(fortune.getMessage()); } }); !engine.run(Tasks.seq(getFortune, printFortune)); !printFortune.await();
Here will compose together two tasks, also in sequence using Tasks.seq.The getFortune task will make a non-blocking request to our /fortunes resource.
Async ClientFortuneClient.java Task<Response<Fortune>> getFortune = parSeqClient.createTask(new FortunesBuilders.get().id(1L)); !final Task<Void> printFortune = Tasks.action("printFortune", new Runnable() { @Override public void run() { Response<Fortune> response = getFortune.get(); Fortune fortune = getResponseEntity(); System.out.println(fortune.getMessage()); } }); !engine.run(Tasks.seq(getFortune, printFortune)); !printFortune.await();
Here will compose together two tasks, also in sequence using Tasks.seq.The getFortune task will make a non-blocking request to our /fortunes resource.
The printFortune task will print the result of the getFortune task to stdout.
Async ClientFortuneClient.java Task<Response<Fortune>> getFortune = parSeqClient.createTask(new FortunesBuilders.get().id(1L)); !final Task<Void> printFortune = Tasks.action("printFortune", new Runnable() { @Override public void run() { Response<Fortune> response = getFortune.get(); Fortune fortune = getResponseEntity(); System.out.println(fortune.getMessage()); } }); !engine.run(Tasks.seq(getFortune, printFortune)); !printFortune.await(); We’ll use a ParSeq engine
directly here to execute the async flow.
Async ClientFortuneClient.java Task<Response<Fortune>> getFortune = parSeqClient.createTask(new FortunesBuilders.get().id(1L)); !final Task<Void> printFortune = Tasks.action("printFortune", new Runnable() { @Override public void run() { Response<Fortune> response = getFortune.get(); Fortune fortune = getResponseEntity(); System.out.println(fortune.getMessage()); } }); !engine.run(Tasks.seq(getFortune, printFortune)); !printFortune.await();
If our client is a command line application, we need to wait for our async tasks to complete before exiting.
Run it again!
Run it again!
“Today’s your lucky day!”
Run it again!
“Today’s your lucky day!”
Much better. Now we can scale.
Next Steps
To learn more, try the Rest.li Quickstart Tutorial
!!
For more details on ParSeq, see the ParSeq Wiki