AJAX Logging with Exponential Backoff

Posted by: Clarity Blogs: ASP.NET, on 13 Jul 2009 | View original | Bookmarked: 0 time(s)

Ive been on something of a JavaScript kick lately. Despite its flaws, its a really expressive language that lacks you pack a lot of functionality into just a little bit of (still readable!) code. To that end, I spent some time this morning fiddling around with client-side logging. Specifically, I wanted to be able to make AJAX calls to log user actions from the browser back to the server. This wasnt really driven by any actual project need, but I think it could be potentially useful in a few scenarios. Maybe you have a public-facing app, and you want to test usability; something like this could make it easy to figure out how people are navigating around your site. This could be especially useful if the site is AJAX-heavy already, or if its some kind of mash-up with a lot of calls to third parties.

How do we get started? Lets build a simple page with a button:

<input id="btn" type="button" value="Click Me!" />

And this is how we want to make our calls:

CLARITY.log('clicked the button at ' + new Date());
Note the use of a global object to act as our namespace. This is a good idea because it lets you minimize your footprint on the global namespace. (I learned this from JavaScript master Douglas Crockford, who wrote the best book on the subject Ive come across.) Well hook these up using a little jQuery. Dont fret if youre not too familiar with it; just believe me when i tell you this code will bind a callback to the buttons click event:
$(function () {
    $('#btn').click(function () {
        CLARITY.log('clicked the button at ' + new Date());
    });
});
Lets go take a look at how CLARITY.log is defined.
var CLARITY = {
    log: function (message) {
        //do some logging, probably
    }
};

As in previous examples, were taking advantage of JavaScripts support for object and function literals to create an object with a property (log) on it. The log property is mapped to a function that takes a message parameter and (we hope!) does something interesting with it. First, lets define a web method for our logging function to call:

[WebMethod]
public static void Log(string msg)
{
    // logging happens here
}

Ive just defined this as a page method. If you were really building something like this out, you might make it a web service call, instead. The difference is pretty transparent from the client-side, so Im not going to go into it here. Im also not going to flesh this method out. Server-side logging is a pretty well-explored problem. This is the place to do it.

Lets take a first cut at our JavaScript logging method:

var CLARITY = {
    log: function (message) {
        $.ajax({
            type: 'POST',
            url: 'Default.aspx/Log',
            data: JSON.stringify({ msg: message }),
            contentType: 'application/json; charset=utf-8',
            dataType: 'json'
        });
    }
};

Were using jQuery again to make an AJAX call. I like jQuerys AJAX interface; I think it strikes a good balance between simplicity and giving you complete control over your request. Many of the parameters we specify here are sort of boilerplate for making AJAX calls to ASP.NET (normally, I wrap AJAX calls in a helper method to mitigate this). For more information on this, Dave Wards blog is a great resource (this post in particular). The call to JSON.stringify comes from Douglas Crockfords JSON utility. Its kind of overkill in this scenario, but I thought Id point it out, since its pretty useful for formatting data for AJAX calls.

To be honest, this is really all we need for logging. However, I thought it might be cool to add some fault tolerance. Maybe (for whatever reason) youd prefer not to lose any of these logging messages when your server hiccups. We might try to solve that problem with an error handling function that tries again:

var CLARITY = {
    log: function (message) {
        var tryWrite = function () {
            $.ajax({
                type: 'POST',
                url: 'Default.aspx/Log',
                data: JSON.stringify({ msg: message }),
                contentType: 'application/json; charset=utf-8',
                dataType: 'json',
                error: tryWrite
            });
        }
    }
};

Note that what weve done here is wrap our AJAX call in a function that quasi-recursively specifies itself as the error handler for the call. This means that every returned error message will immediately fire off another attempt. Note also that message is not used as a parameter for tryWrite; this is unnecessary because tryWrite creates a closure with message in scope.

This isnt bad for a first attempt, but it has obvious problems. What if the server is down for more than a few seconds? This is going be firing off logging attempts continuously, and it will only get worse as the user continues to click around the page. This will tie up their bandwidth and bombard your server (once it recovers). It will also increase the CPU strain from their browser (though my testing shows this may be negligible). All bad things.

What can we do differently? We might try adding a timer to at least give a buffer between attempts. Something like this:

var CLARITY = {
    log: function (message) {
        var tryWrite = function () {
            $.ajax({
                type: 'POST',
                url: 'Default.aspx/Log',
                data: JSON.stringify({ msg: message }),
                contentType: 'application/json; charset=utf-8',
                dataType: 'json',
                error: function () {
                    setTimeout(tryWrite, 5000);
                }
            });
        }
        tryWrite();
    }
};

Here tryWrite is wrapped in a call to setTimeout specifying tryWrite as the callback to be invoked in 5000ms. This will help to some extent. At least we wont be firing off requests continuously until we succeed. But this still isnt ideal. As the life of the page wears on, more and more of these will build up, and things will still eventually bog down. We can alleviate these problems with exponential backoff. Youve probably seen something like this before. The idea is that, as we keep failing, we introduce longer and longer delays between attempts. This helps us to avoid spamming the server. Heres a simple implementation:

var CLARITY = {
    log: function (message) {
        var delay = 1000, tryWrite;
        tryWrite = function () {
            $.ajax({
                type: 'POST',
                url: 'Default.aspx/Log',
                data: JSON.stringify({ msg: message }),
                contentType: 'application/json; charset=utf-8',
                dataType: 'json',
                error: function () {
                    setTimeout(tryWrite, delay);
                    delay *= 2;
                }
            });
        }
        tryWrite();
    }
};

Note that, like tryWrite and message, delay is caught up in our closure, making it something like an implicit parameter. Here we use our delay to set our timer and then double the delay for the next (potential) failure. This guarantees that older failures will remain dormant most of the time, keeping the user from making too many extraneous calls.

This is still a fairly naive implementation, but I thought Id throw it out there as an example. Hopefully someone can get some ideas from it. A more clever version might be more conservative after a failure (or maybe some small number of failures). It could go into a kind of hibernation state where it pings the server periodically and queues incoming messages to be sent once the server is back online. But thats probably a topic for another post.

Hope this helps.

Category: Ajax | Other Posts: View all posts by this blogger | Report as irrelevant | View bloggers stats | Views: 2424 | Hits: 23

News Categories

.NET | Agile | Ajax | Architecture | ASP.NET | BizTalk | C# | Certification | Data | DataGrid | DataSet | Debugger | DotNetNuke | Events | GridView | IIS | Indigo | JavaScript | Mobile | Mono | Patterns and Practices | Performance | Podcast | Refactor | Regex | Security | Sharepoint | Silverlight | Smart Client Applications | Software | SQL | VB.NET | Visual Studio | W3 | WCF | WinFx | WPF | WSE | XAML | XLinq | XML | XSD