Thursday, 29 December 2011

Update On Long Running Server Tasks using ASP.Net, jQuery and Ajax.

          Often in distributed applications, There is a need to update the client on a long running server task. This becomes inherently difficult because of the ‘statelessness’ of the HTTP protocol, In that, Between every HTTP Get there is no state maintained on the server (well at-least in most cases). Therefore, when a client request starts and owns a process which is lengthy (often like uploading something, or communicating to/from a remote database server etc..), periodic updates on the progress of the lengthy process is very imperative for a good user experience, specially in the web 2.0 generation.

I’m going to discuss a few techniques to achieve this using an ASP.Net Ajax development model considering a Windows Server scenario here. Following are the most commonly used techniques for achieving it.

  • Server Push
  • Client Pooling

Server Push (COMET)

In very simple words, COMET is a technique/web application model where the server keeps the HTTP connection open for longer durations. This allows the server to push updates to the client. This can be achieved using techniques like HTTP Streaming, Hidden iFrame and XMLHttpRequests (AJAX) to name a few.

  • Advantages
    1. Saves Traffic –  The server needs to make just one call to the client to send an update.
    2. Efficient – From a logical architecture point of view of the application, it is a very simple and efficient way of updating the client. (This might come at a cost, see below). A very good example of COMET technique is the meebo messenger service. (check it out here) You can also check out the Google implementation here (Etherpad). This was used in Google Wave.
    3. Although a little tricky to achieve this using native ASP.Net architecture, You can leverage  HTML 5 Web Sockets to achieve it. A C# Implementation - Nugget is available on codeplex.
    4. Can be ideal for real-time or close to real-time update scenarios.
  • Drawbacks
    1. Server Memory – The more state that you would want to maintain on the server, the more server memory is required. This again, depends on a lot of factors. For instance, consider applications like Hotmail and Gmail, Here, the number of concurrent connections open can be  significantly higher, which means a lot of server state should be maintained, which will eventually require significant memory. On the other hand, if you are writing an ASP.Net Web App running on a intranet, your concurrent connections may be a couple of tens of them., which also means less server memory compared to the above scenario.
    2. Server I/O API -  This process itself depends on what I/O API is leveraged on the server. Asynchronous processes generally use Non-Blocking I/O, utilizing all cores on the server and increasing efficiency. Async process are handled by a separate thread, which also means the application can do something else while this Async thread executes. Blocking I/O requires a thread per connection and as the concurrent connections increase, the no. of threads processing that request will also increase. A good debate on Non-Blocking vs. Blocking I/O can be found here.
    3. COMET style model implementations have a tendency to run into difficulties as the application is scaled up for large user bases, simply because the server needs to maintain multiple connections between the same client and server. (This is not a claim, I will be supporting all these explanations with concrete implementations and load tests soon.)

Client Polling

Client Polling is done from the browser through AJAX requests. The browser repeatedly polls the server for updates through Ajax calls and updates the client from the server’s response.

  • Advantages
    1. Simple to implement
    2. Suitable for non-real-time applications (like chat-clients)
  • Drawbacks
    1. Latency -  There can be latency between the client’s poll and the server’s response. See the below scenarios. This is mainly due to the unpredictable progress of the server’s process and the constant time interval of the poll.
    2. Not efficient use of traffic and resources – Not every poll to the server might get a concrete response. Don’t get me wrong here, I’m not trying to say that not every pool might get a response, but between two consecutive polls, the state of the long process on the server would not have changed, forcing the server to repeat the previous response. i.e let’s say the the client polls the server every 20 milliseconds for an update, Now, the server might return a different value only after 100 milliseconds, by which time you can imagine the client would have polled the server 4 times already rendering the previous 4 calls to the server without result. This is not efficient use of traffic and reduces your application efficiency. There can be another scenario here, Consider the server’s process returns a state change response every 10 milliseconds. Now, if the client polls the server at intervals of 20 milliseconds, the server’s process has changed significantly since then, adding to a loss of update between every poll.

Both COMET and Client Polling have their ups and downs. As you’ve seen, there strictly isn’t a stereotypical way of achieving the end result, The most imperative of these points are that when it comes to what’s best to YOU, you might have to think on the above lines, scoping out various factors of your distributed application and then your choice will be obvious.

Below is my implementation of a client AJAX Polling to server for a update on the long running server process.

Example : Visual Studio 2010, Asp.NET 4.0, jQuery 1.7, jQuery UI 1.8

I’ve written a very simple AJAX client pooling demo here. The server starts a ‘BackgroundWorker’ process Asynchronously and the client polls the server at regular intervals by making AJAX calls and getting the updates on the server progress. I’ve used jQuery UI to draw a progress bar and update it asynchronously after every poll response from the server. I’ve used jQuery in the client to make use of $.ajax({}); call and the server response is a JSON data type. This is a demo implementation only. Here, the BackgroundWorker object is made static. Therefore this implementation sends the progress of the same job to all the client requests which are polling for an update. An enterprise application would need to have a separate web service running on the server to do the ‘long task’ and each request will then need to be INDEPENDENTLY made aware of it’s Async process’s progress.

Default.aspx.cs

  1: using System;
  2: using System.ComponentModel;
  3: using System.Threading;
  4: using System.Web.Services;
  5: 
  6: namespace ClientPoolServerProgress
  7: {
  8:     public partial class _Default : System.Web.UI.Page
  9:     {
 10:         static BackgroundWorker bwProcess; 
 11:         public static int Percentage { get; set; }
 12: 
 13:         protected void Page_Load(object sender, EventArgs e)
 14:         {
 15:             Percentage = 0;
 16:         }
 17: 
 18:         [WebMethod()]
 19:         public static int GetProgress()
 20:         {
 21:             return Percentage;
 22:         }
 23: 
 24:         protected void btnClick_Click(object sender, EventArgs e)
 25:         {
 26:             bwProcess = new BackgroundWorker
 27:             {
 28:                 WorkerReportsProgress = true,
 29:                 WorkerSupportsCancellation = true
 30:             };
 31: 
 32:             bwProcess.DoWork += new DoWorkEventHandler(bwProcess_DoWork);
 33:             bwProcess.ProgressChanged += new ProgressChangedEventHandler(bwProcess_ProgressChanged);
 34:             bwProcess.RunWorkerCompleted += new RunWorkerCompletedEventHandler(bwProcess_RunWorkerCompleted);
 35: 
 36:             bwProcess.RunWorkerAsync("AsyncWorker");
 37:         }
 38:         
 39:         void bwProcess_DoWork(object sender, DoWorkEventArgs e)
 40:         {  
 41:             for (int i = 0; i <= 100; i++)
 42:             {
 43:                 if (bwProcess.CancellationPending) 
 44:                 { 
 45:                     e.Cancel = true; 
 46:                     return;
 47:                 }
 48:                 bwProcess.ReportProgress(i);
 49:                 Thread.Sleep(20);
 50:             }
 51: 
 52:             e.Result = "100 %";
 53:         }
 54: 
 55:         void bwProcess_ProgressChanged(object sender, ProgressChangedEventArgs e)
 56:         {
 57:             Percentage = e.ProgressPercentage;
 58:         }
 59: 
 60:         void bwProcess_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
 61:         {
 62: 
 63:         }
 64:     }
 65: }
 66: 


Default.aspx



  1: <%@ Page Title="Home Page" Language="C#" MasterPageFile="~/Site.master" AutoEventWireup="false"
  2:     CodeBehind="Default.aspx.cs" Inherits="ClientPoolServerProgress._Default" 
  3:     Async="true" Buffer="True" EnableSessionState="ReadOnly" AsyncTimeout="300"%>
  4: 
  5: <asp:Content ID="HeaderContent" runat="server" ContentPlaceHolderID="HeadContent">
  6:     <script type="text/javascript">
  7: 
  8:         $(document).ready(function () {
  9:             updateProgress();
 10:             $("#pBar").progressbar({ value: 0 });
 11:         });
 12: 
 13:         function updateProgress() {
 14:             $.ajax({
 15:                 type: "POST",
 16:                 url: "Default.aspx/GetProgress",
 17:                 data: "{}",
 18:                 contentType: "application/json; charset=utf-8",
 19:                 dataType: "json",
 20:                 async: true,
 21:                 success: function (msg) {
 22:                     $("#lblProgress").text(msg.d);
 23:                     $("#pBar").progressbar({ value: msg.d });
 24: 
 25:                     if (msg.d < 100) {
 26:                         setTimeout(updateProgress, 10);
 27:                     }
 28:                 },
 29:                 cache:false 
 30:             });
 31:         }
 32:     </script>
 33: </asp:Content>
 34: <asp:Content ID="BodyContent" runat="server" ContentPlaceHolderID="MainContent">
 35:     <div>
 36: 
 37:      <asp:UpdatePanel ID="updPanel" UpdateMode="Always" runat="server" >
 38:         <ContentTemplate>
 39:             <table class="ui-widget ui-widget-content" width="400">
 40:                 <tr>
 41:                     <th class="ui-widget-header" style="width:150px;" >
 42:                         Item
 43:                     </th>
 44:                     <th class="ui-widget-header">
 45:                         Details
 46:                     </th>
 47:                 </tr>
 48:                 <tr>
 49:                     <td>
 50:                         Percent Progress
 51:                     </td>
 52:                     <td>
 53:                         <label id="lblProgress"> </label> 
 54:                     </td>
 55:                 </tr>
 56:                 <tr>    
 57:                     <td>
 58:                         Progress Indicator
 59:                     </td>
 60:                     <td>
 61:                         <div id="pBar"></div>
 62:                     </td>
 63:                 </tr>
 64:     
 65:                 <tr>
 66:                     <td colspan="2">
 67:                             <asp:button ID="btnClick" runat="server" Text="Start" CssClass="ui-button" 
 68:                              onclick="btnClick_Click" />
 69:                     </td>
 70:                 </tr>
 71:             </table> 
 72:           </ContentTemplate>
 73:     </asp:UpdatePanel>
 74:     </div>
 75: </asp:Content>
 76: 


Capture



Download Code (C#)


5 comments: