Problems caused by concurrent asynchronous calls to web services

.NET, ASP.NET, Technical stuff, WPF
See comments

What’s COMET?

When an Internet Application needs to be updated regularly, the only possible choice is to use polling. The HTTP protocol makes it impossible to have the server “push” data to the client. The faster the client needs to be updated, the faster the poll must be. Of course, there are limits, for example due to the network speed, the load, etc.
A possible alternative is known under the name COMET. (Note: COMET is usually used as an alternative to AJAX, and AJAX implies the use of JavaScript (the J in AJAX), running in a web browser. This article, however, uses a WPF XBAP application as client.)
COMET supposes the following scenario:
  1. Client sends a web request (AJAX, web service…) to the server.
  2. Server blocks the response until an event occurs, or until a specified timeout.
  3. Client receives the response, processes it (error due to the timeout, or valid value).
  4. Client immediately sends the next request to the server.

COMET problems

COMET poses a certain number of problems, for example the added strain to the web server (because multiple connections remain open at all time), and also the fact that many HTTP 1.1 stack implementations comply with the specifications and offer only 2 concurrent connections from a given client to a given server (see RFC2616, section 8.1.4).
There are two problems caused by COMET in the specific case of a .NET client (WinForms, WPF):
  1. One instance of a web service proxy can only process one request concurrently. So if we want to fully use both HTTP connections available, we need two web service instances.
  2. The web server will queue requests coming from the same client and process them after one another. If the COMET request is waiting, and another conventional request arrives, it will not return until the COMET response has been sent!
Problem number 1 is easy to solve: Simply create one web service proxy instance for COMET and another one for conventional requests. Make sure in the client-side code that each proxy sends a new request only after the current response has been received.
Problem number 2 is trickier. We need to let the server think that each proxy is a different client (though they run in the same client application). Since the Sessions are identified by the SessionID, which is saved on the client in a cookie, we need to assign a different CookieContainer to each proxy. This way, concurrent requests are possible. However, this has disadvantages: The number of Sessions on the server increases (so more resources will be needed), and also the two proxies do not share the Session state (obviously).
For the moment, I don’t see a simple solution to this problem. One possibility is to handle Session state management “manually” on the server, by using a static Dictionary and custom-made session IDs. When using this solution, one needs to be extra careful when cleaning up on session timeout, in order to avoid memory leaks on the web server. In any case, this problem must be considered with care when using COMET on a ASP.NET web server.

Demonstration

In .NET, Web Service References are easily added to a project using the “Add web reference” context menu.
Adding a web reference
This operation connects to the web service’s WSDL (Web Service Definition Language) file, and uses the information to create a proxy. This class exposes the exact same methods as the web service. These methods can be used for synchronous calls. The proxy class also exposes an additional set of the same methods, prefixed with the word “Async”. For each such method, an event is exposed too. The method can be used to call the web service asynchronously, and the event uses a delegate (as is usual in .NET 2) to be executed when the asynchronous response is received.
For example, if we have the following web service (server-side):
[WebService( Namespace = "http://www.galasoft-lb.ch" )]
[WebServiceBinding( ConformsTo = WsiProfiles.BasicProfile1_1 )]
[ToolboxItem( false )]

public class WebService1 : System.Web.Services.WebService
{
  [WebMethod(EnableSession=true)]
  public string ReturnNow()
  {
    return "Counter value: " + IncrementCounter();
  }

  [WebMethod(EnableSession=true)]
  public string ReturnAfterSleep( int iSleep )
  {
    System.Threading.Thread.Sleep( iSleep );
    return "Counter value (after sleep): " + IncrementCounter();
  }

  private int IncrementCounter()
  {
    if ( Session[ "Counter" ] == null )
    {
      Session[ "Counter" ] = 1;
    }
    Session[ "Counter" ] = (int) Session[ "Counter" ] + 1;
    return (int) Session[ "Counter" ];
  }
}


Given these methods, the proxy class (client-side) will have 2 synchronous methods, two asynchronous methods and two events:
Intellisense for proxy class
 
The “ReturnAfterSleep” web method simulates a COMET behaviour. Since each request runs in its own Thread, you can let a Thread sleep and call other web methods simultanously in the web service. For example, a possible WPF client could be:
namespace XAMLBrowserApplication1
{
  public partial class Page1 : System.Windows.Controls.Page
  {
    // Create the web service proxy instance. Note that the proxy's

    // full name is XAMLBrowserApplication1.WebService1.WebService1
    WebService1.WebService1 service1
      = new WebService1.WebService1();

    public Page1()
    {
      // Define event handlers for the asynchronous method calls
      service1.ReturnAfterSleepCompleted
        += new WebService1.ReturnAfterSleepCompletedEventHandler(
          service1_ReturnAfterSleepCompleted );

      service1.ReturnNowCompleted
        += new WebService1.ReturnNowCompletedEventHandler(
          service1_ReturnNowCompleted );
      
      InitializeComponent();
    }

    // Calls the asynchronous "sleeping" web method

    public void bnGetAsyncSleep_Click( object sender,
      RoutedEventArgs e )
    {
      service1.ReturnAfterSleepAsync( 15000 );
    }

    // Handles the asynchronous response "after sleep"
    void service1_ReturnAfterSleepCompleted( object sender,
      WebService1.ReturnAfterSleepCompletedEventArgs e )
    {
      lblTest.Content = e.Result;
    }

    // Call the asynchronous "non-sleeping" web method

    public void bnGetAsync_Click( object sender,
      RoutedEventArgs e )
    {
      service1.ReturnNowAsync();
    }

    // Handles the asynchronous response "without sleep"
    void service1_ReturnNowCompleted( object sender,
      WebService1.ReturnNowCompletedEventArgs e )
    {
      lblTest2.Content = e.Result;
    }
  }
}
However, if we try using the same web service proxy while a connection is already running, an exception is thrown on the client. In the example above, if we press on “bnGetAsyncSleep” and then, while the Thread is sleeping on the client, on “bnGetAsync”, we get the following error message (this is problem number 1 described above):
XBAP error in IE7
 
—> System.ArgumentException:
There was an error during asynchronous processing.
Unique state object is required for multiple asynchronous
simultaneous operations to be outstanding.
—> System.ArgumentException: Item has already been added.
Key in dictionary: ‘System.Object’
Key being added: ‘System.Object’
What happened is that the same proxy instance is being added to a Hashtable when two simultanous requests are processed. To avoid that, it is necessary to create additional proxy instances.
Additionally, we want to use the Session state on the server (counter). To do this, it is necessary that the SessionID cookie be saved in both proxy instances, or else the server will consider that each proxy is a different client, and will start two separate sessions. So we must create one CookieContainer instance, and assign this same instance to both proxies. So the corrected client code becomes:
// Create two web service proxy instances. Note that the proxy's
// full name is XAMLBrowserApplication1.WebService1.WebService1
WebService1.WebService1 service1
  = new WebService1.WebService1();
WebService1.WebService1 service2
  = new WebService1.WebService1();
  
(...)
  
public Page1()
{
  // Add the same CookieContainer instance

  // to both proxies
  CookieContainer cookieContainer = new CookieContainer();
  service1.CookieContainer = cookieContainer;
  service2.CookieContainer = cookieContainer;

  // Define event handlers for the asynchronous method calls
  service1.ReturnAfterSleepCompleted
    += new WebService1.ReturnAfterSleepCompletedEventHandler(
      service1_ReturnAfterSleepCompleted );

  // The second proxy instance is used for the "non sleeping" method

  service2.ReturnNowCompleted
    += new WebService1.ReturnNowCompletedEventHandler(
      service2_ReturnNowCompleted );

  InitializeComponent();
}

(...)

// Call the asynchronous "non-sleeping" web method
public void bnGetAsync_Click( object sender,
  RoutedEventArgs e )
{
  service2.ReturnNowAsync();
}

// Handles the asynchronous response "without sleep"

void service2_ReturnNowCompleted( object sender,
  WebService1.ReturnNowCompletedEventArgs e )
{
  lblTest2.Content = e.Result;
}

The code above uses two proxy instances, each of them using the same CookieContainer instance. So for the server, both proxies are in fact the same client. And now we can see problem number 2 appearing: If we press the “bnGetAsync” button, this creates a new session on the server. The cookie with the SessionID is saved in the CookieContainer. Then press the “bnGetAsyncSleep” button, which causes the server to sleep for 15 seconds. Immediately after that, press the “bnGetAsync” button again: Instead of returning immediately as expected, the response arrives only after the “Sleep” response is received. The server processed the requests sequentially, because they both have the same SessionID.
Creating and assigning a different CookieContainer to each proxy takes care of that, but then the both proxy instances don’t share the Session state anymore.

Conclusion

Using COMET is quite tricky. Having one HTTP connection “sleep” for some time is known to cause additional strain on the web server and is “wasting” one HTTP connection when often only two are available per client per server.
Additionally, like we demonstrate here, COMET causes problems specific to .NET implementations. While one problem can be quite easily solved, the other (web server processing requests sequentially when they originate from a single web client) needs careful consideration when implementing the server side.

This post was imported from my old blog. Original comments screenshot:

2016-01-24_14-41-58

Previous entry | Next blog entry

Comments for Problems caused by concurrent asynchronous calls to web services