Microsoft .NET Framework 3.5 Windows Communication Foundation: Sessions and Instancing
- 9/24/2008
- Before You Begin
- Lesson 1: Instancing Modes
- Lesson 2: Working with Instances
- Chapter Review
- Chapter Summary
- Key Terms
- Case Scenarios
- Suggested Practices
- Take a Practice Test
Lesson 2: Working with Instances
The instance mode WCF uses is just the start of working with instances. You can manipulate a number of details to improve the performance and scalability of a WCF service. WCF provides throttling and quota capabilities that can help prevent denial of service (DoS) attacks as well as ensure that the servers aren’t overloaded by handling requests. Along the same lines, you can control the activation and deactivation of the instances used to process requests to a degree that is finer than the default functionality.
Not only does WCF allow for performance to be protected, some attributes can be set to demarcate operations. The demarcation ensures that, where necessary, some operations cannot be completed before or after other operations. This is not a complete workflow management function, but it does allow a service to ensure that a particular operation is called first and that no operations can be called after a finalize operation has been performed.
Protecting the Service
When WCF is deployed in the real world (where real is defined as a distributed environment in which requests arrive at a pace that is outside of your control), a number of potential problems can arise. Some of the performance differences associated with the different instancing modes have already been covered. However, beyond pure performance problems, WCF services have to contend with some of the same problems that a Web site has to contend with. This includes the potential for being flooded with client requests, similar to a denial of service attack.
Denial of service attacks are attempts to deplete the resources required by the service to process incoming requests to the point that no additional resources are available. The type of depleted resources can include any scarce resource the service uses. WCF provides a number of ways to mitigate the problem through either throttling requests or applying quotas to the resource.
Throttling
The goal of throttling is twofold. First, it prevents the service host from being overrun by a flood of requests. Second, it enables the load on the WCF service (and the server on which the service is running) to be smoothed out. In both cases, the intent is to place a limit on the number of incoming requests so that the service will be able to handle them in a timely manner.
The default WCF setting for throttling is to have none at all. When throttling is engaged, WCF will check the current counters for each request that arrives. If the configured settings are exceeded, WCF automatically places the request in a queue. As the counters come down below the threshold, the requests are then retrieved from the queue in the same order and presented to the service for processing. The result of this is that, in many cases, the observed behavior for a service that has reached its maximum is to have the client request time out.
Three settings in the service behavior control the number of requests the service host will be allowed to process simultaneously. Each of these is defined in the ServiceThrottlingBehavior section of the configuration file. The following paragraphs describe the three settings and are followed by an example of how you can configure them.
MaxConcurrentCalls
The MaxConcurrentCalls value specifies the number of simultaneous calls the service will accept. The default value is 16 calls. Of the three settings, this is the only one that covers all the types of requests that arrive.
MaxConcurrentSessions
The MaxConcurrentSessions value determines the maximum number of channels requiring sessions that the service will support. The default value for this setting is 10 session-aware channels. Any attempt to create a channel beyond this maximum will throw a TimeoutException. Because this setting is concerned with session-aware channels only, if the binding is not session-aware (such as the basicHttpBinding), this setting has no impact on the number of requests that can be processed.
MaxConcurrentInstances
The MaxConcurrentInstances setting sets the maximum number of instances of the service implementation object that will be created. The default value for this setting is Int32.MaxValue, and the impact this value has on the service depends on the mode. If the mode is per call, this is the same as MaxConcurrentCalls because each call gets its own instance. If the mode is per session, the setting works the same as MaxConcurrentSessions. For singleton mode, the value of the number of instances is always 1, so the setting is really only useful when the IInstanceContextProvider is being used.
The following segment from a configuration file demonstrates how you can configure these settings:
<behaviors> <serviceBehaviors> <behavior name="throttlingBehaviort"> <serviceThrottling maxConcurrentCalls="10" maxConcurrentInstances="10" maxConcurrentSessions="5"/> </behavior> </serviceBehaviors> </behaviors>
You can set the same configuration through code. The following segments demonstrate the technique:
' VB Dim host As New ServiceHost(GetType(UpdateService), _ New Uri("http://localhost:8080/UpdateService")) host.AddServiceEndpoint("IUpdateService", _ New WSHttpBinding(), String.Empty) Dim throttlingBehavior As New ServiceThrottlingBehavior() throttlingBehavior.MaxConcurrentCalls = 10 throttlingBehavior.MaxConcurrentInstances = 10 throttlingBehavior.MaxConcurrentSessions = 5 host.Description.Behaviors.Add(throttlingBehavior) host.Open() // C# ServiceHost host = new ServiceHost( typeof(UpdateService), new Uri("http://localhost:8080/UpdateService")); host.AddServiceEndpoint( "IUpdateService", new WSHttpBinding(), String.Empty); ServiceThrottlingBehavior throttlingBehavior = new ServiceThrottlingBehavior(); throttlingBehavior.MaxConcurrentCalls = 10; throttlingBehavior.MaxConcurrentInstances = 10; throttlingBehavior.MaxConcurrentSessions = 5; host.Description.Behaviors.Add(throttlingBehavior); host.Open();
As has been mentioned, when the throttling limits are reached, the client will throw an exception. Specifically, the exception the client receives is the previously mentioned Time-outException. Because this one exception fits all scenarios (that is, the same exception is raised regardless of which of the throttling settings caused the problem), it is left up to you to discover the cause. A couple of hints can help. If the problem is caused by the concurrent sessions limit, you will most likely see the exception raised within the SendPreamble method. If it turns out that the Send method is the source of the time out, it is more likely to be caused by the maximum concurrent calls limit.
Reading Throttling Settings
It is possible to read (but not update) the current throttling settings after the service host has been opened. Applications do this, typically to provide diagnostic information about the service. You do this by accessing the dispatcher for the service, which is responsible for implementing the throttling, so it makes sense that the dispatcher would have all the information close at hand.
The ServiceHost class exposes a collection of dispatchers in the ChannelDispatchers property. This is a strongly typed collection of ChannelDispatched objects. The ChannelDispatcher object has a property called ServiceThrottle. Through the ServiceThrottle object, you have access to all the throttling properties, including MaxConcurrentCalls, MaxConcurrentInstances, and MaxConcurrentSessions. The following code demonstrates this technique:
' VB Dim dispatcher As ChannelDispatcher = _ TryCast(OperationContext.Current.Host.ChannelDispatchers(0), _ ChannelDispatcher) Dim throttle as ServiceThrottle = dispatcher.ServiceThrottle Trace.WriteLine(String.Format("MaxConcurrentCalls = {0}", _ throttle.MaxConcurrentCalls)) Trace.WriteLine(String.Format("MaxConcurrentSessions = {0}", _ throttle.MaxConcurrentSessions)) Trace.WriteLine(String.Format("MaxConcurrentInstances = {0}", _ throttle.MaxConcurrentInstances)) // C# ChannelDispatcher dispatcher = OperationContext.Current.Host.ChannelDispatchers[0] as ChannelDispatcher; ServiceThrottle throttle = dispatcher.ServiceThrottle; Trace.WriteLine(String.Format("MaxConcurrentCalls = {0}", throttle.MaxConcurrentCalls)); Trace.WriteLine(String.Format("MaxConcurrentSessions = {0}", throttle.MaxConcurrentSessions)); Trace.WriteLine(String.Format("MaxConcurrentInstances = {0}", throttle.MaxConcurrentInstances));
Quotas
The quota mechanism available through WCF involves controlling the amount of memory used by the service host and the various service implementation objects. The premise behind a DoS attack that is aimed at memory is to find a way to make the processing of the request(s) allocate an inordinately large amount of memory. As additional requests arrive (whether good ones or malicious ones), an OutOfMemoryException or a StackOverflowException might be raised.
When you apply a quota to a WCF service, the QuotaExceededException is raised. However, instead of this exception causing the service to terminate (as the out of memory or stack overflow condition might), the message being processed is simply discarded. The service then processes the next request and carries on.
A number of settings affect the level of quota.
MaxReceivedMessageSize
The MaxReceivedMessageSize value (along with the other settings associated with quotas) is set on the binding directly. It controls how large a message size can be. The default value is 65,536 bytes, which should be sufficient for most messages. You can set this value through either code or configuration. The following demonstrates a configuration element that will set the value of the maximum message size to 128,000 bytes:
<bindings> <netTcpBinding> <binding name="netTcp" maxReceivedMessageSize="128000" /> </netTcpBinding> </bindings>
You can set this value imperatively also, as demonstrated in the following code sample:
' VB Dim binding As New NetTcpBinding() binding.MaxReceivedMessageSize = 128000 Dim host As New ServiceHost(GetType(UpdateService), _ New Uri("net.tcp://localhost:1234/UpdateService")) host.AddServiceEndpoint("IUpdateService", _ binding, String.Empty) host.Open() // C# NetTcpBinding binding = new NetTcpBinding(); binding.MaxReceivedMessageSize = 128000; ServiceHost host = new ServiceHost( typeof(UpdateService), new Uri("net.tcp://localhost:1234/UpdateService")); host.AddServiceEndpoint( "IUpdateService", binding, String.Empty); host.Open();
ReaderQuotas
The ReaderQuotas property of the binding sets limits on the complexity of the messages received by the service. They protect that service from memory-based denial of service by specifying a set of criteria within which all messages must fall. Table 10-1 contains a list of the properties that can be set on the ReaderQuotas object and their meanings.
Table 10-1 ReaderQuotas Properties
Property |
Default |
Description |
MaxDepth |
32 |
The maximum depth to which the nodes in the message can go. This is like saying that the XML that represents the message can have no more than 32 generations (where a parent node and a child node make up a generation) at the deepest point in the schema. |
MaxStringContentLength |
8192 |
The longest that any string value in the message can be. A string value would be the value of an attribute or the value of the inner text for any node. |
MaxArrayLength |
16384 |
The maximum number of elements that can appear in a single array. |
MaxBytesPerRead |
4096 |
The maximum number of bytes returned by each call to Read while the message is processed. |
MaxNameTableCharCount |
16384 |
The maximum number of characters that can appear in a table name. |
Demarcating Operations
Conceptually, a session simply means that the service can determine which client a request is coming from. This enables the service to maintain state between the individual requests. However, there are times when the order in which the operations are executed actually matters, and this requirement calls for an extension to the sessioning mechanism.
The idea of needing to maintain the order in which methods are called might seem a little bizarre. After all, in the vast majority of business applications, the client is quite capable of ensuring this, but in many cases, the ability of the client to dictate the order of operations is not as solid as you might think.
Consider, for example, any HTTP-based binding. Although it would seem that if MethodA is invoked before MethodB, then in every case, MethodA will be executed on the service before MethodB. However, suppose MethodA and MethodB are executed on different threads. Still, isn’t it possible to ensure that the two threads are synchronized to the point that the client can guarantee execution order?
The answer is no. When using an HTTP-based binding, there is no guarantee of the order of arrival. Even though the client executes MethodA before MethodB (on different threads; this doesn’t apply to synchronous calls), HTTP will not guarantee that the request associated with MethodA will arrive at the service prior to MethodB. Unless the service is enlisted in the mechanism to guarantee operation order, no such guarantee can be made.
Consider the following Service contract:
' VB <ServiceContract(SessionMode:=SessionMode.Required)> _ Public Interface IProcessOrders <OperationContract> _ Sub InitializeOrder(customerId As Integer) <OperationContract> _ Sub AddOrderLine(productId As String, _ Quantity As Integer) <OperationContract> _ Function GetOrderTotal() As Double <OperationContract> _ Function SubmitOrder() As Boolean End Interface // C# [ServiceContract(SessionMode = SessionMode.Required)] public interface IProcessOrders { [OperationContract] void InitializeOrder(int customerId); [OperationContract] void AddOrderLine(string productId, int quantity); [OperationContract] double GetOrderTotal(); [OperationContract] bool SubmitOrder(); }
The business rules associated with this interface are that the first method to be called has to be InitializeOrder. This instantiates an Order object and populates the fields with default values. Then the AddOrderLine method must be called at least once (although it can be called multiple times). Next, GetOrderTotal is called to calculate the order totals. Finally, the SubmitOrder method is called. This last method also closes the session.
WCF provides a mechanism that enables contract designers to indicate operations, which cannot be the first or last method, by setting the IsInitiating and IsTerminating properties on the OperationContract attribute. If IsInitiating is set to true for a method and no session has been established when that method is called, a session is created. If a session already exists, the method is called within that session.
If IsTerminating is set to true for a method, when the method completes, the session is closed. This is not the same as disposing of the service instance, however. The client still needs to execute the Close method on the proxy to close the connection. However, any subsequent methods on this proxy will be rejected with an InvalidOperationException.
By using these properties, it is possible to mark the start and end of an operation. The default value for IsInitiating is true, and the default value for IsTerminating is false. Because of this, the settings that are required in the sample interface should be set as follows (changes shown in bold):
' VB <ServiceContract(SessionMode:=SessionMode.Required)> _ Public Interface IProcessOrders <OperationContract> _ Sub InitializeOrder(customerId As Integer) <OperationContract(IsInitiating:=False)> _ Sub AddOrderLine(productId As String, _ Quantity As Integer) <OperationContract(IsInitiating:=False)> _ Function GetOrderTotal() As Double <OperationContract(IsInitiating:=False, IsTerminating:=True)> _ Function SubmitOrder() As Boolean End Interface // C# [ServiceContract(SessionMode = SessionMode.Required)] public interface IProcessOrders { [OperationContract] void InitializeOrder(int customerId); [OperationContract(IsInitiating=false)] void AddOrderLine(string productId, int quantity); [OperationContract(IsInitiating=false)] double GetOrderTotal(); [OperationContract(IsInitiating=false, IsTerminating=true)] bool SubmitOrder(); }
Consider how these settings will work. Of the four methods, only InitializeOrder can start a session. So, if one of the other methods is called prior to InitializeOrder, it throws an InvalidOperationException. The remaining methods can then be called in any order required, with one exception. If SubmitOrder is called because of the IsTerminating property, the session is closed.
Instance Deactivation
The details of the issues associated with sessions and service instances are, not surprisingly, more complicated. Consider Figure 10-3, which represents a view closer to reality of a service.
Figure 10-3 How instances and hosts are related
As you can see in Figure 10-3, the service instance is actually loaded into a Context object, and the session information routes client messages not to the specific instance but to the context.
When a session is created, the service host creates a new context. This context is terminated when the session ends. This means that the lifetime of the context matches the instance hosted within it by default. WCF enables the developer of the service to separate the two lifetimes.
WCF goes a step further in that you can create a context that has no instance at all. The way to control context deactivation is through the ReleaseInstanceMode property of the OperationBehavior attribute.
You can set various values in that ReleaseInstanceMode to identify when the service instance should be released in relation to a particular method control. The choices are BeforeCall, AfterCall, BeforeAndAfterCall, or None.
The default release mode is None. This means that the service instance continues to exist as method requests arrive and processes are returned. This is the mode that you have come to expect from a service instance.
If the release mode is set to BeforeCall, a new instance is created with the beginning of the call. If a service instance already exists, it is deactivated and the Dispose method called on it. The client is blocked while this is going on because it is assumed to be important to have the new service instance available to process the request. This style is normally used when the method allocates a scarce resource, and it must to be certain that any previous use has been cleaned up.
If the release mode is set to AfterCall, the current service instance is deactivated and disposed of when the method is completed. This would normally be set when the method is deallocating a scarce resource. The idea is that, after the method is finished, the service instance is disposed of immediately, ensuring that the resource will be available for the next caller.
The last release mode is BeforeAndAfterCall. This mode disposes of any existing service instance prior to executing the method and then disposes of the just created service instance after the method is finished. You might recognize this as the same as the per call instance mode. The difference, however, is that this mode can be set on an individual method, so you can configure one method to be (basically) per call instancing, whereas the other methods in the service can use a different instancing model.
You can define the release mode declaratively, using code that looks like the following:
' VB Public Class UpdateService Implements IUpdateService <OperationBehavior( _ ReleaseInstanceMode:=ReleaseInstanceMode.BeforeAndAfterCall)> _ Public Sub Update() ' Implementation code goes here End Sub End Class // C# public class UpdateService : IUpdateService { [OperationBehavior(ReleaseInstanceMode=ReleaseInstanceMode.BeforeAndAfterCall)] public void Update() { // Implementation code goes here } }
You also have the option of making a run-time decision to deactivate the current service instance (when the method is complete). The instance context exposes a ReleaseServiceInstance method. When called, the current instance is marked to be deactivated and disposed of after the method is finished. The instance context is part of the operation context, so the way to call this method looks like the following:
' VB OperationContext.Current.InstanceContext.ReleaseServiceInstance() // C# OperationContext.Current.InstanceContext.ReleaseServiceInstance();
This technique is intended to provide a high level of granularity to optimize the service. However, as is true with many such techniques, the normal course of events doesn’t require this level of effort. It is better to design and develop your application using more standard techniques, falling back on these only if performance and scalability goals are not being met.
Lab: Throttling and Demarcation
In this lab, you will work with two separate functions. The first exercise will illustrate some of the throttle configuration you can perform. The effect that some of the settings (such as large request limits, maximum levels in deserialization, and so on) have on incoming requests can be a little challenging to illustrate. As a result, the exercise shows the ones that can be easily demonstrated.
The second exercise will create a service with demarcated operations and demonstrate the exceptions raised when the specified order is violated.
Exercise 1 Throttle WCF Requests
In this first exercise, you will restrict the number of simultaneous instances a service can create. You will use a service similar to the one constructed in the lab for Lesson 1 in this chapter.
Navigate to the <InstallHome>/Chapter10/Lesson2/Exercise1/<language>/Before directory and double-click the Exercise1.sln file to open the solution in Visual Studio.
The solution consists of two projects. They are as follows:
The DemoService project, a simple WCF service library that implements the ISession interface. This interface consists of a single method (GetSessionStatus) that returns a string indicating the number of times the method has been called within the current service instance.
The TestClient project, a Console application that generates a request for the service and displays the result in the Console window.
You can find the settings for throttling a service in the configuration file for the service.
In Solution Explorer, double-click the App.config file in the DemoService project.
Locate the behavior named ThrottleBehavior in the serviceBehaviors element.
The throttle is set in the serviceThrottling element.
Set the maximum number of concurrent instances to 2 by adding the following XML to the behavior element within the serviceBehaviors element:
<serviceThrottling maxConcurrentInstances="2" />
Ensure that TestClient is set as the startup project, and then launch the application by pressing F5.
When prompted for an instance ID, enter a value of 123 and press Enter.
This creates the first instance.
When prompted for the instance ID again, enter a value of 456 and press Enter.
This creates the second instance.
Finally, when prompted for the instance ID again, enter a value of 789.
This is the third instance and, rather than displaying a string on the console, it will wait. In fact, it will wait until the timeout value has been exceeded and an exception is thrown.
Choose Stop Debugging from the Debug menu to end the application.
Exercise 2 Demarcate Operations
As mentioned, WCF provides some functionality aimed at regulating the order in which operations can be executed. In this exercise, you will configure the service to use this capability. Then you will modify the client to test not only the successful path but also an execution order that would violate the configured order.
Navigate to the <InstallHome>/Chapter10/Lesson2/Exercise2/<language>/Before directory and double-click the Exercise2.sln file to open the solution in Visual Studio.
The solution consists of two projects. They are as follows:
The DemoService project, a simple WCF service library that implements the ISession interface. This interface consists of three methods (FirstMethod, GetSessionStatus, and LastMethod), each of which returns a string indicating which method has been called.
The TestClient project, a Console application. The application generates a request for the service and displays the result in the Console window.
In Solution Explorer, double-click the ISession file.
You will notice that three methods are defined within the contract.
To start, configure FirstMethod to be the first operation called, by setting the IsInitiating property on the OperationContract attribute to true.
The IsInitiating property for the other methods in the interface also must be set to false, but you do that shortly.
Change the declaration of FirstMethod (as shown in bold) to look like the following:
' VB <OperationContract(IsInitiating:=True)> _ Function FirstMethod() As String // C# [OperationContract(IsInitiating = true)] string FirstMethod();
Now the IsInitiating property for the GetSessionStatus method must be set to false.
Change the method declaration (as shown in bold) to the following:
' VB <OperationContract(IsInitiating:=False)> _ Function GetSessionStatus() As String // C# [OperationContract(IsInitiating = false)] string GetSessionStatus();
The third method also must have IsInitiating set to false. However, the intent is for this method to be the last method called. For this reason, the IsTerminating property must be set to true.
Change the method declaration (as shown in bold) to the following:
' VB <OperationContract(IsInitiating:=False, IsTerminating:=True)> _ Function LastMethod() As String // C# [OperationContract(IsInitiating = false, IsTerminating = true)] string LastMethod();
In Solution Explorer, double-click Program.cs or Module1.vb.
Notice the order in which the methods are called. This is what is expected by the configuration in the service.
Ensure that TestClient is set as the startup project, and then launch the application by pressing F5.
Note that the messages appear as expected.
End the application.
To modify the order of the method calls, move the call to GetSessionStatus so that it occurs before the call to FirstMethod.
The body of the using block in the Main method should look like the following:
' VB Console.WriteLine(proxy.GetSessionStatus()) Console.WriteLine(proxy.FirstMethod()) Console.WriteLine(proxy.LastMethod()) // C# Console.WriteLine(proxy.GetSessionStatus()); Console.WriteLine(proxy.FirstMethod()); Console.WriteLine(proxy.LastMethod());
Launch the application by pressing F5.
You will find that an InvalidOperationException or an ActionNotSupportedException is raised with the GetSessionStatus call. The message in the exception indicates that GetSessionStatus was invoked before a method in which IsInitiating has been set to true.
Choose Stop Debugging from the Debug menu to end the application.
Finally, move the call to GetSessionStatus so that it occurs after the call to LastMethod.
The body of the using block in the Main method should look like the following:
' VB Console.WriteLine(proxy.FirstMethod()) Console.WriteLine(proxy.LastMethod()) Console.WriteLine(proxy.GetSessionStatus()) // C# Console.WriteLine(proxy.FirstMethod()); Console.WriteLine(proxy.LastMethod()); Console.WriteLine(proxy.GetSessionStatus());
Launch the application by pressing F5.
Again, you will find that an InvalidOperationException is raised with the GetSessionStatus call. This time, the message in the exception indicates that GetSessionStatus was invoked after a method in which IsTerminating has been set to true was called.
Choose Stop Debugging from the Debug menu to end the application.
Lesson Summary
Client endpoint configuration starts from the same address, binding, and contract bases as services do.
If one of the standard bindings is specified, the default values for that binding are used.
You define additional binding behaviors through a behaviorConfiguration section.
If the client supports callbacks, you can define a number of client behaviors through the endpointBehavior section.
All of the configuration that can be performed declaratively can also be performed imperatively.
You can instantiate all the bindings by using the name of a configuration section. Alternatively, you can instantiate the binding separately, assign the desired properties, and then associate it with the proxy.
Lesson Review
You can use the following questions to test your knowledge of the information in Lesson 2, “Working with Instances”. The questions are also available on the companion CD if you prefer to review them in electronic form.
Consider the following segment from a configuration file.
<behaviors> <serviceBehaviors> <behavior name="throttlingBehaviort"> <serviceThrottling maxConcurrentCalls="15" maxConcurrentInstances="10" maxConcurrentSessions="5"/> </behavior> </serviceBehaviors> </behaviors>
Which of the following statements is true?
The service can accept no more than fifteen simultaneous requests.
The service can accept no more than ten simultaneous requests.
The service can accept no more than five simultaneous requests.
There is no limit to the number of simultaneous requests the service can accept.
Consider the properties of the OperationContract that demarcate an operation. Which of the following statements is false?
You can specify which method must be the first one called within the service.
You can ensure that no methods in the service can be called after a specific method is called.
You cannot ensure the order of all the methods in a service unless two or fewer methods are exposed.
You can ensure that a particular method will always be called when the service is finished.