Alterian ACM : Caching… a better approach!

If you have read my last post about our performance problems with the Alterian API, then you may appreciate our dilemma.  The API was slow and Alterian ACM provides no out-of-the-box caching solutions to make it faster.  When I spoke to Alterian, the best suggestion they advised was to use squid for Windows, a proxy tool that is unsupported and development finished on it in 2010.  This obviously wasn’t something I could advise a client to use, so I had to come up with an alternative approach.

Our aim was pretty simple…. make our page load times less than 50ms. After several days of trying .NET caching, browser caching the only viable way we found to do this was create static HTML versions of our pages.  The site is now live but for confidentiality reasons I can’t include a URL. I can say the site runs stupidly quickly and most page loads are less than 10ms!

If you want to implement something similar, then you’ll need to get the finished html created by .NET and store it to disc. First, you need to create a custom base class that all your pages/templates inherit from.  You’ll need to override your Render method as seen below:

NOTE : We also had a custom multi-language requirement,  so I also needed to dynamically modify all the href tags and append a language query string at the end in order to make browser caching work (otherwise if a user switched languages the browser would not reload it).  I won’t go into details about how I did that but have a look at the HTMLAgility pack, it’s about 10 lines of code if you need to do something similar.

      if (!Page.IsPostBack)
{
var sb = new StringBuilder();
var sw = new StringWriter(sb);
var hWriter = new HtmlTextWriter(sw);
// Get HTML
base.Render(hWriter);
var pageResult = sb.ToString();
var doc = new HtmlDocument();
doc.LoadHtml(pageResult);
// Custom href manioulation bit (ignore)
foreach (var link in doc.DocumentNode.SelectNodes("//a[@href]"))
{
ParseUrl(link);
}
var fileLocation = GetCacheFileName();
SaveFile(fileLocation.ToString(CultureInfo.InvariantCulture), doc);
html = doc.DocumentNode.OuterHtml;
writer.Write(html);
}
else
{
//  If it's a post back you don't want to pass the static page back, otherwise your dynamic plug-ins will break.
base.Render(writer);
}

So, now on each page load a static HTML file will be created and saved to disc.  The next hurdle is to return the HTML on a page request.  At first I did this in Init of the base class but I found a flaw.  If you do a Response.End .Net throw a thread abortion error (more info).  The next thing I knew my error logs were being filled with these unneeded exceptions.  To get round it, I put the check into an HTML Module.  Adding it to a module means when Response.End is called because the page life cycle hasn’t kicked off yet, no exception is thrown.  Using WCAT for measuring, we went from being able to handle 700 request per second to over 3000 just from moving the code from init to a module, so it’s definitely worth implementing this way if performance is important to you:

NOTE: IsBelowThreshold is a custom method that creates the creation date, if it is over an hour  the cache is re-created.

public class CacheHandler : IHttpModule
{
public void OnBeginRequest(object o, EventArgs args)
{
var app = (HttpApplication)o;

var isPostBack = string.Equals(app.Request.HttpMethod, “POST”);
if (!isPostBack)
{
var PageCacheName = GetCacheFileName(app.Context);
if (File.Exists(PageCacheName))
{
if (IsBelowThreshold(PageCacheName, null))
{
var file = File.ReadAllText(PageCacheName);
app.Context.Response.Clear();
app.Context.Response.Write(file);

AddBrowserCache(app);

HttpContext.Current.ApplicationInstance.CompleteRequest();
}
else
{
File.Delete(PageCacheName);
}
}
}

private static void AddBrowserCache(HttpApplication app)
{
try
{
var config = (CachingSection)app.Context.GetSection(“merchantCaching/Caching”);
if (config != null)
{
var doub = config.CachingTimeSpan.TotalSeconds;
app.Context.Response.Cache.SetExpires(DateTime.Now.AddSeconds(doub));
app.Context.Response.Cache.SetMaxAge(config.CachingTimeSpan);
app.Context.Response.Cache.SetValidUntilExpires(false);
}
}
catch (Exception ex)
{
Logger.Error(ex);
}
}
}

Enjoy!

Jon D Jones

Software Architect, Programmer and Technologist Jon Jones is founder and CEO of London-based tech firm Digital Prompt. He has been working in the field for nearly a decade, specializing in new technologies and technical solution research in the web business. A passionate blogger by heart , speaker & consultant from England.. always on the hunt for the next challenge

More Posts