2015
02
February

Going deep with ETags

ETags are a great way to enable a dynamic web application to take advantage of browser caching. However, outside of the ShallowETagHeaderFilter, there is very little guidance on how a developer might incorporate ETags into a Spring web application.

Not too long ago we were asked to help solve a performance problem with a Spring MVC application. The application was largely dynamic in nature with each page being fronted by a Spring Controller. The client had already configured the ShallowEtagHeaderFilter and the application was correctly returning a 304 response code for unchanged pages.

Unfortunately, performance was still poor even when the server responded with a content unchanged response. This was due to the manner in which the ShallowEtagHeaderFilter determines content changes. The filter works by hashing the response body and comparing said hash with the incoming ETag. If the incoming ETag matches the hash, the filter responds with the 304 response code. There are two practical consequences to this that impact performance. First, because the response is hashed in memory, it precludes the use of chunked encoding of the response. Second, the web application needs to perform all the work of rendering a response in order to determine if content is changed or not. In cases where page rendering is expensive, but transmission is cheap, the use of this filter will not result in significant performance gains.

There was a common characteristic to the pages in our client’s application that helped us develop an approach for “deep ETags.” Many pages were expensive to render, cheap to transmit, but it also turned out that most had surprisingly cheap ways in which we could determine if a page had expired. For example, a page that contains several master/detail records of data from a RDBMS was taking almost 3000 ms to render and hash whereas a simple query to determine the last time any row has changed that might appear on the page could be executed in a dozen milliseconds. It is this gap that our approach takes advantage of.

Instead of generically hashing response bodies as in ShallowEtagHeaderFilter, we will annotate our controller methods indicating a custom AwesomeETagilator for the method. It will be the job of the AwesomeETagilator to compute an ETag for the given inputs. The clear advantage of this is that we compute the ETag before we render the response. When ETags match, we skip the entire render process.

The annotation and referenced interface looks like this:

@Retention(RetentionPolicy.RUNTIME)
@Target(value={ElementType.METHOD})
public @interface AwesomeETagger {
	Class<? extends AwesomeETagilator>[] eTagger();
	String spel = "";
}
public interface AwesomeETagilator {
	//answer back an etag, yo!
	public String tagilate(HttpServletRequest request, HttpServletResponse response, Object handler);
}

How do we put these to use? An example implementation of AwesomeETagilator might look like this:

@Component
public class CurrentUserLocationListTagilator implements AwesomeETagilator {
	@Autowired
	private LocationService locationService;

	public String tagilate(HttpServletRequest request, HttpServletResponse response, Object handler) {
		UserAccount userAccount = SecurityUtils.getCurrentUser();
		Date latestChange = locationService.getLatestLocationDate(userAccount);
		return String.valueOf(latestChange.getTime());
	}
}

Where we might annotate a controller:

@Controller
@RequestMapping("/secured/location")
public class LocationController {
	@Autowired
	private LocationRepository locationRepository;

	public String getListPage(Model model, @PathVariable("page") int page, UserAccount userAccount) {
		Page<Location> locations = locationRepository.findByUserAccountOrderByCreatedOnDateDesc(newPageRequest(page, 20), userAccount);
		model.addAttribute("userAccount", userAccount);
		model.addAttribute("locations", locations);
		model.addAttribute("hasPreviousPage", locations.hasPreviousPage());
		model.addAttribute("hasNextPage", locations.hasNextPage());
		return "secured/location/list";
	}

	@AwesomeETagger(eTagger={CurrentUserLocationListTagilator.class})
	@RequestMapping(method=RequestMethod.GET)
	public String getList(Model model, UserAccount userAccount) {
		return getListPage(model, 0, userAccount);
	}
}

Now that we have our annotation and AwesomeETagilator implementation, how does this all work? We need a Spring MVC interceptor to glue this all together. The interceptor and configuration are below.

<!-- Register "global" interceptor beans to apply to all registered HandlerMappings -->
<mvc:interceptors>
    <bean class="com.isostech.bw.web.AwesomeETagilatorInterceptor" />
</mvc:interceptors>

…and finally the interceptor itself:

public class AwesomeETagilatorInterceptor extends HandlerInterceptorAdapter implements ApplicationContextAware {

	private static final String HEADER_ETAG = "ETag";
	private static final String HEADER_IF_NONE_MATCH = "If-None-Match";

	private ApplicationContext context = null;
	private ExpressionParser parser = new SpelExpressionParser();

	/*
	 *
	 * (non-Javadoc)
	 * @see org.springframework.web.servlet.handler.HandlerInterceptorAdapter#preHandle(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, java.lang.Object)
	 */
	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
		if (request.getMethod().equals("GET") && handler != null && handler instanceof HandlerMethod) {
			AwesomeETagger methodAnnotation = ((HandlerMethod) handler).getMethodAnnotation(AwesomeETagger.class);
			if (methodAnnotation != null) {
				String requestETag = request.getHeader(HEADER_IF_NONE_MATCH);
				// generate new etag
				String newETag = "";
				for (int i = 0; i < methodAnnotation.eTagger().length; i++) {
					Class<? extends AwesomeETagilator> beanType = methodAnnotation.eTagger()[i];
					AwesomeETagilator foo = context.getBean(beanType);
					newETag += foo.tagilate(request, response, handler);
				}
				if (!methodAnnotation.spel.isEmpty()) {
					newETag += evaluate(request, methodAnnotation.spel);
				}
				if (newETag.equals(requestETag)) {
					response.setStatus(304); // no change
					return false;
				}
				response.setHeader(HEADER_ETAG, newETag);
			}
		}
		return super.preHandle(request, response, handler);
	}

	@Override
	public void setApplicationContext(ApplicationContext context) throws BeansException {
		this.context = context;
	}

	private String evaluate(HttpServletRequest request, String expression) {
		Expression exp = parser.parseExpression(expression);
		EvaluationContext context = new StandardEvaluationContext(request);
		return exp.getValue(context, String.class);
	}
}

You might notice there is a feature in the interceptor we haven’t discussed. In addition to the array of AwesomeETagilator implementations contributing to the ETag, we may include a SpEL expression that will contribute to the ETag. For example, assuming we have a Spring Bean that can answer the version of our application, we might decide to expire pages when we deploy a new application release with an expression.

@AwesomeETagger(eTagger={CurrentUserLocationListTagilator.class}, spel="@versionService.version")

In this example, both the application version and the result of CurrentUserLocationListTagilator will compose the ETag.

The Downside

Since the CurrentUserLocationListTagilators and the SpEL expressions will be evaluated with each request, the implementations of each must inexpensive. Moreover, this approach only makes sense when the expense of the ETag contributors is a small fraction of the expense of the resulting response. However, where you have a dynamic application with slowly changing data, this approach can dramatically impact performance by leaning on browser caching for dynamic pages.

Share with your peeps...Share on FacebookShare on Google+Tweet about this on TwitterShare on LinkedIn
Tagged with: , — Posted in Software Development

Leave a Reply