Friday, 24 September 2010

WCF per-call services and sessions

I ought not to admit this but it turns out there was a rather large gap in my understanding of WCF that I’ve only just noticed. I was under the assumption that client proxies didn’t use sessions until explicitly told to do so, which would allow them to remain alive without impacting service scalability. Suffice to say I was very wrong. This may sound like an esoteric/pointless discovery but it’s actually really important for application reliability.

I came across this problem because one of the features in the application I’m currently working on uses a variant of Bea Stollnitz’s virtualisation solution to page search results. I realised that if I did a search, went to lunch, and then came back and scrolled through the search results then the proxy would fault. I was already sort-of aware that proxies timed out but I wasn’t sure why. For per-call services it seemed counter-intuitive given that they’re not maintaining an open channel to the service. It turns out (as stated very clearly in chapter 4 of my favourite WCF book) that even per-call services use sessions unless told not to do so. This comes with a default inactivity timeout of 10 minutes after which the proxy will throw a CommunicationObjectFaultedException if any attempt is made to use it. The inactivity timeout can be changed on the service side, for example:

<bindings>
<wsHttpBinding>
<binding>
<reliableSession enabled="true" inactivityTimeout="00:00:05" />
</binding>
</wsHttpBinding>
</bindings>

but in the meantime the service will keep a session identifier (available in the proxy’s InnerChannel.SessionId property) for every client until either the timeout is reached or the client proxy calls the close method.

So, thought I, there are three things I need to do with all future per-call services:

  1. Mark the service as per-call so it doesn’t create a service-side object for each proxy
  2. Set the session mode to ‘not allowed’
  3. Disable reliable sessions

The per-call attribute bit is easy – just pop the InstanceContextMode property on the ServiceBehaviour attribute (not that concurrency mode is set to single – this is to enforce isolation in service calls):

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall,
ConcurrencyMode=ConcurrencyMode.Single))]
public class InvoiceManager : IInvoiceManager
{
// Service implementation here
}

Likewise the session mode – this should be set on the contract:

[ServiceContract(SessionMode = SessionMode.NotAllowed)]
interface IInvoiceManager
{
// Contract methods here
}

and finally the reliable session bit:

<system.serviceModel>
<bindings>
<wsHttpBinding>
<binding>
<reliableSession enabled="false" />
</binding>
</wsHttpBinding>
</bindings>
</system.serviceModel>

An alternative is to leave sessions as enabled (the default) . There are several things that can go wrong so the following code block can be taken as boilerplate for exception handling:

try
{
// Call proxy method here
}
catch (EndpointNotFoundException)
{
// Service may be down or network is playing up
}catch (MessageSecurityException)
{
// Covers a multitude of sins (typically when using custom security)
// But generally revolves around faulted channels and reliability issues
// Thrown by session timeout errors in a custom security environment (particularly where certificates are concerned)
// But also when load balancing without sticky sessions
}
catch (CommunicationObjectFaultedException)
{
// If channel wasn't faulted before the call then most likely the timeout error
// otherwise this shouldn't happen (especially with defensive coding checking the proxy's state)
}
catch (TimeoutException)
{
// The service has timed out
// This shouldn't really happen as services should be responsive
// Reconsider the service design
}
catch (SecurityAccessDeniedException)
{
// Let the user know they're being naughty
}
catch (CommunicationException)
{
// anything else goes here
}

Obviously it doesn’t include contracted faults as they should be specifically coded for and it does assume that channels that are known to have faulted aren’t being used. In most cases it’s probably sensible to wrap all this up in a generic handler which notifies the user of the problem, but in some cases you may want to attempt to restore the proxy and try again.  Michele Bustamante has a such a solution here.

However, this is not without its problems.  The one that springs to mind (because it’s caused me problems recently) is caused by round-robin load balancing.  Even when I turn off sessions and reliability on wsHttpBinding, WCF still expects server-affinity (despite the fact that it shouldn’t).  To get round this I’ve been forced to use the only binding that is truly stateless: basicHttpBinding.  In effect I’m going back to ASMX web services but with WCF plumbing.  I’m not overly concerned about the security implications at the moment because I’m using Windows Authentication with ASP.NET roles (as described in this article by ScottGu) which in WCF gives me transport security:

      <basicHttpBinding>
        <binding>
          <security mode="TransportCredentialOnly">
            <transport clientCredentialType="Windows" />
          </security>
        </binding>
      </basicHttpBinding>

It doesn’t give me message security or reliability, but as it’s point-to-point I won’t lose too much sleep over this.  Nor will I trouble myself over the lack of support for flowing transactions across service boundaries – as it’s not something I need in this scenario.  What’s more, with IIS 7 compression turned on it goes like sh*t off a shovel.

No comments:

Post a Comment