A REST request might fail for several reasons:
Temporary failures can be masked by issuing the request multiple times.
Usually, the client quits after a few retries to
avoid blocking the application forever,
for example, in
case the server has crashed.
Note:
Transient failures are temporary issues that resolve themselves shortly.
Jersey (JAX-RS) exposes request failures to clients in the form of a Java exception: javax.ws.rs.ProcessingException
A try{ } catch{} block can be used to retry the request automatically after a small amount of time.
Note: Waiting a bit before retrying the request prevents a too agressive client behavior and allows some time for the transient error condition disappear.
Setting the client timeout values...
protected static final int READ_TIMEOUT = 5000;
protected static final int CONNECT_TIMEOUT = 5000;
ClientConfig config = new ClientConfig();
config.property(ClientProperties.READ_TIMEOUT, READ_TIMEOUT);
config.property( ClientProperties.CONNECT_TIMEOUT, CONNECT_TIMEOUT);
Client client = ClientBuilder.newClient(config);
In the future, other changes to client behavior will be done the someway.
protected static final int MAX_RETRIES = 10;
protected static final int RETRY_SLEEP = 1000;
@Override
public String createUser(User user) {
WebTarget target = client.target( serverURI ).path( RestUsers.PATH );
for (int i = 0; i < MAX_RETRIES; i++)
try {
Response r = target.request()
.accept(MediaType.APPLICATION_JSON)
.post(Entity.entity(user, MediaType.APPLICATION_JSON));
if( r.getStatus() == Status.OK.getStatusCode() && r.hasEntity() )
// SUCCESS
return r.readEntity(String.class);
else {
System.out.println("Error, HTTP error status: " + r.getStatus() );
break;
}
} catch (ProcessingException x) {
sleep( RETRY_SLEEP );
}
return null; // Report failure
}
The sample code above needs to be repeated for all operations of all services!
Can we make it more general?
Of course!!!
private String clt_createUser(User user) {
Response r = target.request()
.accept(MediaType.APPLICATION_JSON)
.post(Entity.entity(user, MediaType.APPLICATION_JSON));
if( r.getStatus() == Status.OK.getStatusCode() && r.hasEntity() )
return r.readEntity(String.class);
else
return null;
}
protected <T> T reTry(java.util.function.Supplier<T> func) {
for (int i = 0; i < MAX_RETRIES; i++)
try {
return func.get(); // Success
} catch (ProcessingException x) {
sleep(RETRY_SLEEP);
} catch (Exception x) {
// Handle other errors
break;
}
return null; // Failure
}
Note:
This method can be part of super class inherited by all service clients.
public String createUser(User user) {
return reTry( () -> clt_createUser( user ));
}
public User getUser(String userId, String password) {
return reTry( () -> clt_getUser( user, password ));
}
We are making use of Java Lambda Expressions...
() -> clt_createUser( user ) is a function that returns a result, making it compatible with the functional interface Supplier<T> used as the parameter of the reTry generic method.
Jersey reports to clients any unhandled exception thrown in the scope of
an implementation method of a service as 500 Internal Error.
The default Jersey runtime behavior is to suppress the actual exception details, including type or stack trace, making it very difficult to diagnose the source of the problem.
Jersey provides a way to customize how server-side exceptions are reported before a response is sent to the client.
For instance, the class below is a custom exception mapper that will allow the stack trace of an exception to show up on server logs.
public class GenericExceptionMapper implements ExceptionMapper<Throwable> {
@Override
public Response toResponse(Throwable ex) {
if (ex instanceof WebApplicationException wex) {
Response r = wex.getResponse();
if( r.getStatus() == Status.INTERNAL_SERVER_ERROR.getStatusCode())
ex.printStackTrace();
return r;
}
ex.printStackTrace();
return Response.status(Status.INTERNAL_SERVER_ERROR)
.entity(ex.getMessage()).type(MediaType.APPLICATION_JSON).build();
}
}
The code will still report the error to the client as 500 Internal Error.
However, it also prints the exception stack trace to the server output, making it easier to find which class, method and line of code caused the problem.
To use a custom exception mapper, we have register it in addition to the services exposed by the server, like so:
ResourceConfig config = new ResourceConfig();
config.register(UsersResource.class);
config.register(GenericExceptionMapper.class);
String ip = InetAddress.getLocalHost().getHostAddress();
String serverURI = String.format(SERVER_URI_FMT, ip, PORT);
JdkHttpServerFactory.createHttpServer( URI.create(serverURI), config);
Tester to test your RestUsers service;Build the Docker image, using the usual maven command;
mvn clean compile assembly:single docker:build
Launch the server;
docker network create -d bridge sdnet (if necessary)
docker run --rm -h users-1 --name users-1 --network sdnet -p 8080:8080 sd2122-tp1-xxxxx-yyyyy
Create the client container:
docker run -it --network sdnet sd2122-tp1-xxxxx-yyyyy /bin/bash
Create a new user.
In the client container shell, type:
java -cp /home/sd/sd2122.jar sd2122.aula3.clients.CreateUserClient http://users-1:8080/rest nmp "Nuno Preguica" nmp@nova.unl.pt 12345
Confirm the request succeeded and returned: nmp
Stop the server.
CTRL-C or execute in another terminal:
docker rm -f users-1
Execute step 5 again.
This time, since the server is not running, the client should output:
FINE: ProcessingException: java.net.UnknownHostException: users-1
Launch the server again (step 3)
The client should finish and return: nmp
Download this lab's project (if necessary);
In the main method of the UsersServer.java file, uncomment the following line:
config.register(GenericExceptionMapper.class);
Build the Docker image, using the usual maven command;
mvn clean compile assembly:single docker:build
Launch the server;
docker network create -d bridge sdnet (if necessary)
docker run --rm -h users-1 --name users-1 --network sdnet -p 8080:8080 sd2122-tp1-xxxxx-yyyyy
Create the client container:
docker run -it --network sdnet sd2122-tp1-xxxxx-yyyyy /bin/bash
Try the SearchUsersClient:
In the client container shell, type:
java -cp /home/sd/sd2122.jar sd2122.aula3.clients.SearchUsersClient http://users-1:8080/rest nmp
The client should report:
Error, HTTP error status: 500
In the server, the top line of stack trace shown should be:
at sd2122.aula3.server.resources.UsersResource.searchUsers(UsersResource.java:96)
This is the method, class file and line number where the uncaught exception was thrown.
Undo Step 2 (by commenting the line);
Repeat Steps 3, 4, 5 and 6.
This time the output of the server will only show that the request reached the server. It will not show that an exception was thrown while processing the request.
The Tester runs your service and performs a series of tests to check if your the implementation complies to the specification.
Apply the changes you did in Lab 2 to the Users service of Lab 3 project.
Adapt the provided trab.props file to your project, making sure class names and network ports match
your implementation. Check the Tester documentation if unsure.
Build the docker image of your completed service;
mvn clean compile assembly:single docker:build
Run the Tester script;
Linux:
chmod a+x test-sd-tp1.sh (once)
./test-sd-tp1.sh -image sd2122-tp1-xxxxx-yyyyy -sleep 10 -log ALL
Windows:
`test-sd-tp1.bat -image sd2122-tp1-xxxxx-yyyyy -sleep 10 -log ALL`