Thursday, May 28, 2009

Caching portlets in ALUI 6.x

I've been developping portlets (gadgets) for nearly ten years now, starting with Plumtree Portal Server 3.5. A the time, there was a very nice document called "The Gadget Book" with all the details about the task of developping gadgets as they were called then. This PDF has been replaced by other versions since, but I still remember a few good practices for having a fast and reliable portal.

Among other things, there was the caching strategies to implement on the portlet side. The portal uses the standard HTTP mechanisms for calling content from the portlet server, as described in RFC 2616. Using the HTTP ETag and Last-Modified headers, we could prevent rendering a whole portlet when its content would remain unchanged. It proceeded like this:

- First call of the portlet by the portal. No special header is passed;
- The portlet returns the content to display, and sets the ETag and/or Last-Modified header;
- On the next access to the portlet, when the minimum cache time specified in the portlet configuration is over, and the maximum time not being reached, the portal calls the portlet giving back the content of the previous header;
- The portlet checks if the content has changed since the last call (in my case by comparing the timestamp passed in the Last-Modified header with a timestamp stored in a DB). If the content should be regenerated, I send the full content and give the new value for the headers. If not, I simply return an HTTP error 304 (Not Modified), and the portal would in that case display the content stored in its cache.

This worked perfectly for ages, until version 6.0 was released. On that version, when the 304 error was returned, the portal would display an error instead of displaying the cache (even when the setting "Suppress errors where possible (show cached content instead)" was checked.

I had several emails going back and forth with the Plumtree/BEA support and they finally acknowledge this as a bug. But in version 6.5, which is the one we are currently running (on http://www.myschool.lu/), that bug is still unresolved.

So, I disabled that part of my code, waiting for a solution to come eventually. I'm still waiting ;-) And my portlet is used in even more places than before (it displays content stored in a DB in many community pages) and the cache is key to allowing proper rendering times. The content would change once in a while, but hundreds of users would see the unchanged content in the meantime.

So, I tried to find a solution to this, and I have implemented the following workaround:

- Configure the portlet in this way (like in old times when caching worked):

- In my caching routine, I check the "CSP-Aggregation-Mode" header to see if I'm called as a portlet in a page, or as a standalone page (inside or outside of the gateway). This header can be empty (e.g. when called from outside of the gateway), can contain "Multiple" when displayed as a portlet in a page, or "Single" or "Hosted" when in an independent page accessed through the gateway.
- In the "Multiple" case, I do not return the 304 error but instead a Service Unavailable error (503). As per the setting above, the error is not displayed and the cached content is shown.
- In any other case, I return the 304 error, as a standalone page is properly processed by the browser. In such cases, the caching is done on the browser side and not the portal.

So far, so good. The performance has increased a lot, as could be expected, and the portlet server has more time to do other things than constantly rendering the same content...

Key lines of code:

Const c_sDateFormat As String = "yyyy-MM-dd HH:mm:ss"
Dim bFromPortal As Boolean = (Request.Headers("CSP-Aggregation-Mode") = "Multiple")

If dBrowserDate.ToString(c_sDateFormat) = dObjectDate.ToString(c_sDateFormat) Then
If bFromPortal Then
Response.StatusCode = System.Net.HttpStatusCode.ServiceUnavailable
Else
Response.StatusCode = System.Net.HttpStatusCode.NotModified
End If
Response.End()
End If

Response.Cache.SetCacheability(HttpCacheability.Public)
Response.Cache.SetRevalidation(HttpCacheRevalidation.AllCaches)
Response.Cache.SetLastModified(dObjectDate)

No comments: