HTTP Conditional Updates with JAX-RS
Finally I got the time to write this blog and conclude my previous blog on Concurrency using HTTP Conditional Updates. Blame it on my trip to Belur, Halebidu.
Yes, the title pic is from my trip :-)
This time apart from providing code samples, I have uploaded a complete working project on GitHub. If you want to try it out, the detailed instructions can be found in the README.md.
Recap
In the earlier blog, we discussed how HTTP provides ability to check if certain preconditions like ETag, Last updated timestamp hold for a given resource before preforming update (HTTP PUT).
This helps in avoiding race conditions that can arise due to multiple clients trying to update the same resource.
Let us see how we can implement this using JAX-RS.
Conditional PUT using JAX-RS
Our Customer bean class contains following fields:
public class Customer {
private long id;
private String firstName;
private String lastName;
private String address;
@XmlTransient
private Date createTime;
@XmlTransient
private Date updateTime;
}
Please note, this is not the complete class, checkout the source code at GitHub for full list. Also, createTime, updateTime are not part of our JSON response.
We have our REST APIs in CustomerResource class. Let us first see how getCustomer() API looks:
@GET
@Path("{id}")
@Produces(MediaType.APPLICATION_JSON)
public Response getCustomer(@PathParam("id") long id) {
Customer c = customerService.getCustomer(id);
if (c != null) {
return Response.ok(c).lastModified(c.getUpdateTime()).build();
}
else {
return Response.status(Status.NOT_FOUND).build();
}
}
The only difference with a conventional GET resource API is that we are also returning the update time for the Customer resource in our response.
In case you want to see how to set the ETag, here is another example:
return Response.ok(c).tag(new EntityTag("" + c.hashCode())).build();
I have shown an example using hash code as the ETag but in real implementation you would want to generate an entity tag based on your application requirements.
For this blog, I will use last updated timestamp for handling concurrent updates.
Let us see how a response to GET /customers/1 request would look like:
Let us assume two clients A, B and both performed GET /customers/1 at this point.
Next, both clients try to preform PUT /customers/1 with some change in the attributes. As we saw in the last blog that this can cause second update to overwrite changes made during the first update. This happens because the second client had an outdated representation of the Customer resource.
This is the perfect time to introduce our PUT handler and how it can take care of concurrent updates:
@PUT
@Path("{id}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response updateCustomer(@PathParam("id") long id, Customer customer, @Context Request request) {
Customer c = customerService.getCustomer(id);
if (c != null) {
ResponseBuilder responseBuilder = request.evaluatePreconditions(c.getUpdateTime());
if (responseBuilder == null) {
customer.setId(id);
Customer updatedCustomer = customerService.updateCustomer(customer);
return Response.ok(updatedCustomer).lastModified(updatedCustomer.getUpdateTime()).build();
}
else {
return responseBuilder.build();
}
}
else {
return Response.status(Status.NOT_FOUND).build();
}
}
The key here is the evaluatePreconditions() method. We have passed the current update timestamp for this resource to this method (again we can also provide an EntityTag object). The evaluatePreconditions() method will compare this value with the one passed by Client and decide whether we are good to update (HTTP 200) or not (HTTP 412).
Both Clients, in their PUT request, need to pass a new HTTP header If-Unmodified-Since with the Last Modified timestamp that we got in the response to GET request.
For the client A, this is how the response to PUT /customers/1 request would be:
As seen, the client is able to update the resource and now the Last-Modified timestamp is updated as highlighted.
At this point if client B tries to update the resource with the old representation of the resource, it will fail as shown below:
The response also indicates the current value of Last-Modified timestamp. The correct action for client B would be to perform a GET /customers/1 and then perform PUT operation.
If you see a correlation between HTTP Conditional Updates and Atomic classes’ compareAndSet() method then you are right. Both provide a mechanism to achieve concurrency using Optimistic Locking.