The Channel State Machine


Channels and channel factories share common characteristics that are independent of their run-time functionality. One of the most important characteristics of these different constructs is their common state machine. Every channel and channel factory in a WCF application has a predefined set of states and a predefined set of methods that drive the channel or channel factory through those states.

The ICommunicationObject Interface

At the object-oriented level, one of the ways the WCF type system enforces the uniformity of a common state machine is by mandating that all channels and channel factories implement the System.ServiceModel.ICommunicationObject interface. The ICommunicationObject interface is fairly straightforward:

 public interface ICommunicationObject {   event EventHandler Closed;   event EventHandler Closing;   event EventHandler Faulted;   event EventHandler Opened;   event EventHandler Opening;   void Abort();   IAsyncResult BeginClose(AsyncCallback callback, object state);   IAsyncResult BeginClose(TimeSpan timeout, AsyncCallback callback,                           Object state);   IAsyncResult BeginOpen(AsyncCallback callback, object state);   IAsyncResult BeginOpen(TimeSpan timeout, AsyncCallback callback,                          Object state);   void Close();   void Close(TimeSpan timeout);   void EndClose(IAsyncResult result);   void EndOpen(IAsyncResult result);   void Open();   void Open(TimeSpan timeout);      CommunicationState State { get; } }

Note 

For brevity in this section, I will refer to objects that implement the ICommunicationObject interface as channels, even though channel factories also implement the interface.

Let’s talk first about the methods. As you can see in the interface definition, the ICommunicationObject interface defines methods for opening, closing, and aborting the channel. Notice that the interface definition overloads the synchronous Open and Close methods with methods that accept a TimeSpan. In theory, the parameterless Open and Close methods block until the channel eventually opens or closes. In practice, this is never a good idea, and the overloads that accept a TimeSpan represent a way to dictate the amount of time a caller is willing to wait for the object to open or close. Since it is never a good idea to block indefinitely, waiting for a channel to open or close, it is a good idea for the parameterless Open and Close methods to call the Open and Close methods that do accept a TimeSpan, passing a default TimeSpan as an argument.

Notice also that the ICommunicationObject interface defines asynchronous BeginOpen and BeginClose methods that match the Microsoft .NET Asynchronous Programming Model (APM) pattern. Because opening or closing a channel might result in I/O, it is a good idea to use asynchronous programming for opening and closing a channel. Doing so means that the application uses the thread pool for efficient resource management and the calling thread does not have to block while the actual work of opening or closing the channel is taking place. Notice also that even the BeginOpen and BeginClose methods are overloaded to include a TimeSpan. Like their synchronous cousins, these methods allow the caller to dictate how long they are willing to wait for a channel to open or close. When opening or closing a channel, I greatly prefer and encourage the use of the asynchronous-capable members defined in ICommunicationObject.

The ICommunicationObject interface also defines a read-only property of type CommunicationState. This member is simply a means to query a channel for its location in the channel state machine. You’ll learn more about the channel state machine in the next section, “The CommunicationObject Type.” For now, it is enough to know the possible states, as shown here:

 public enum CommunicationState {       Created,       Opening,       Opened,       Closing,       losed,       Faulted }

The ICommunicationObject interface also defines several events. Like any .NET Framework event, the events defined in ICommunicationObject are a means for other objects to receive notifications of some or all channel state transitions. Notice that the event names correlate to the CommunicationState enumerated type. We’ll look at the timing of these events in the next section.

The CommunicationObject Type

By itself, implementing the ICommunicationObject interface does nothing to enforce consistent state transitions across all channels or channel factories. Instead, it ensures that all channels and channel factories have common members. In practical terms, enforcing consistent behavior across a set of types compels the use of a common base type for implementation inheritance, rather than interface inheritance alone. The System.ServiceModel.Channels. CommunicationObject abstract type serves this purpose.

image from book
Figure 6-2: The channel state machine embodied in CommunicationObject

Note 

For brevity in this chapter, I will once again refer to objects that subclass the CommunicationObject type as channels, even though other types are also derived from this CommunicationObject.

CommunicationObject is a base type for all channels, and the CommunicationObject type implements the ICommunicationObject interface. The Open, Close, and Abort methods on CommunicationObject drive channels through their various states in a consistent manner, as shown in Figure 6-2. More than just an implementation of ICommunicationObject, CommunicationObject also raises ICommunicationObject events at the appropriate time, invokes abstract and virtual methods for derived type implementation, and provides several helper methods for consistent error handling. The next section of this chapter describes the manner in which the CommunicationObject drives channels through different states.

CommunicationObject-Derived Types

In practice, types derived from CommunicationObject should work with the state machine defined in CommunicationObject, should leverage some of its other members for error handling, and of course, should add implementation that fits the needs of that particular derived type. As with any type hierarchy, blindly inheriting from a base type does not by itself ensure the proper use of the base type functionality. When building a channel, it is extremely important to add functionality in the appropriate place and to call methods on the base type correctly.

The CommunicationObject type defines several virtual methods. When a derived type overrides these virtual methods, it is extremely important that the derived type call its base because it is the CommunicationObject implementation that drives state changes and raises events. Failing to make this call means that the state of the derived type will not transition properly, events will not be raised, and the channel will be of little value. It is not required that a type derived from CommunicationObject override these members. Instead, a CommunicationObject-derived type should override these virtual members only when that derived type needs to perform some work in its own implementation.

The following code snippet shows the virtual methods in the CommunicationObject type and how they must be overridden:

 public abstract class CommunicationObject : ICommunicationObject {   // virtual methods shown, others omitted   protected virtual void OnClosed();   protected virtual void OnClosing();   protected virtual void OnFaulted();   protected virtual void OnOpened();   protected virtual void OnOpening(); } sealed class CommunicationObjectDerivedType : CommunicationObject {   // other methods omitted for clarity   protected override void OnClosed() {     // implementation can occur before or after     // the call to the base implementation     base.OnClosed();   }   protected override void OnClosing() {     // implementation can occur before or after     // the call to the base implementation     base.OnClosing();   }   protected override void OnOpened() {     // implementation can occur before or after     // the call to the base implementation     base.OnOpened();   }   protected override void OnOpening() {     // implementation can occur before or after     // the call to the base implementation     base.OnOpening();   }   protected override void OnFaulted() {     // implementation can occur before or after     // the call to the base implementation     base.OnFaulted();   } }

The CommunicationObject type also defines several abstract members that are the primary means by which a channel performs specialized work. The following code snippet describes these members:

 public abstract class CommunicationObject : ICommunicationObject {   // abstract members shown, others omitted   protected abstract void OnOpen(TimeSpan timeout);   protected abstract IAsyncResult OnBeginOpen(TimeSpan timeout,     AsyncCallback callback, Object state);   protected abstract void OnEndOpen(IAsyncResult result);   protected abstract void OnClose(TimeSpan timeout);   protected abstract IAsyncResult OnBeginClose(TimeSpan timeout,     AsyncCallback callback, Object state);   protected abstract void OnEndClose(IAsyncResult result);   protected abstract void OnAbort();   protected abstract TimeSpan DefaultCloseTimeout { get; }   protected abstract TimeSpan DefaultOpenTimeout { get; } }

The only members in the preceding code snippet that should come as a surprise are the DefaultCloseTimeout and DefaultOpenTimeout properties. As a rule, when deciding which overloaded member to call, always choose the one with a TimeSpan parameter. This provides explicit control over the time-out. As it turns out, even the members that do not have a TimeSpan parameter call the member that does have a TimeSpan parameter. In that case, the value used is the value of the DefaultOpenTimeout and DefaultClosedTimeout, accordingly.

The OnOpen, OnClose, and OnAbort methods and their asynchronous siblings are, as their name implies, the place where much of the initialization and cleanup implementation goes in a CommunicationObject-derived type. For example, if you are writing a custom transport channel that uses the User Datagram Protocol (UDP) transport, the code required to initialize the socket should reside in the OnOpen and OnBeginOpen methods. Likewise, the code to tear down the socket should reside in the OnClose, OnBeginClose, and OnAbort methods.

One of the areas that can be confusing when first approaching channels and the channel state machine is the way in which the CommunicationObject interacts with types derived from the CommunicationObject. In my view, understanding these interactions is one of the most important first steps in understanding how channels work. The next sections describe the collaboration between the CommunicationObject base type and derived types for the Open, Close, Abort, and Fault methods. For the sake of simplicity, the following code snippet defines the context for these sections:

 sealed class App {   static void Main() {     MyCommunicationObject myCommObject = new MyCommunicationObject();     // method invocations here   } } sealed class MyCommunicationObject : CommunicationObject {   // implementatation omitted for brevity }

The Open and BeginOpen Methods

As you saw earlier in this chapter, the CommunicationObject defines the Open and BeginOpen methods that open the CommunicationObject-derived type. This section describes what happens as a result of the following code:

 MyCommunicationObject myCommObject = new MyCommunicationObject(); myCommObject.Open();

CommunicationObject: Check Whether State Transition to Open Is Permissible

The Open and BeginOpen methods throw an exception if the State state property is anything other than CommunicationObject.Created. The CommunicationObject type performs these checks by calling the ThrowIfDisposedOrImmutable protected method. If the CommunicationState is CommunicationState.Opened or CommunicationState.Opening, the Open and BeginOpen methods throw an InvalidOperationException. Likewise, if the State is CommunicationState.Closed or CommunicationState.Closing, the Open and BeginOpen methods throw an ObjectDisposedException. It is worth noting that this state check happens in a thread safe manner. The following code snippet describes the implementation of the CommunicationObject.Open method:

 lock (this.thisLock){   // check the state, throw an exception if transition is not OK   this.ThrowIfDisposedOrImmutable();      // other implementation shown in the next section    }

CommunicationObject: If So, Transition State to Opening

If the current state is CommunicationState.Created, the State property transitions to CommunicationState.Opening. The following code snippet shows the code in the CommunicationObject.Open method that transitions the state to CommunicationState.Opening:

 lock (this.thisLock){   // check the state, throw an exception if transition is not OK   this.ThrowIfDisposedOrImmutable();      // transition the CommunicationState      this.state = CommunicationState.Opening;    }

MyCommunicationObject: OnOpening Virtual Method Invoked

If the CommunicationState property transitions to Opening without throwing an exception, the CommunicationObject.Open method invokes the CommunicationObject.OnOpening virtual method. If the CommunicationObject derived type has overridden this method, the OnOpening method on the derived type is invoked. As mentioned earlier, the OnOpening implemention in the derived type must call the OnOpening method on the CommunicationObject type.

CommunicationObject: Opening Event Raised, Delegates Invoked

The OnOpening method on the CommunicationObject type raises the Opening event and invokes the delegates referred to in that event. This is one reason the derived type must call the OnOpening method on the CommunicationObject. The CommunicationObject.Open method will throw an InvalidOperationException if this collaboration does not occur.

MyCommunicationObject: OnOpen Virtual Method Invoked

If the OnOpening method does not throw an exception, the CommunicationObject.Open method invokes the OnOpen method in the derived type. Because the CommunicationObject type defines OnOpen as an abstract method, derived types must implement this method. As mentioned earlier, this is the method that contains the bulk of the work required to initialize the CommunicationObject-derived type.

MyCommunicationObject: OnOpened Virtual Method Invoked

If the OnOpen method returns without throwing an exception, the CommunicationObject.Open method invokes the OnOpened virtual method. If the derived type implements the OnOpened method, the implementation in that derived type is invoked. As with the OnOpening method, it is absolutely critical that the derived type invoke the CommunicationObject.OnOpened method. Failing to do so results in the CommunicationObject.Open method throwing an InvalidOperationException.

CommunicationObject: State Transitions to Opened

The CommunicationObject.OnOpened method, among other things, transitions the State property of the CommunicationObject to CommunicationState.Opened. The only CommunicationState that is permissible before this state transition is CommunicationState.Opening.

CommunicationObject: Opened Event Raised, Delegates Invoked

After the state transitions to Opened, the CommunicationObject.OnOpened method raises the Opened event, thereby invoking any referenced delegates.

The Close and Abort Methods

The CommunicationObject type exposes members that tear down the object. In general, the Close and BeginClose methods are intended for graceful CommunicationObject shutdown, and the Abort method is intended for immediate CommunicationObject shutdown. Notice that the Close method has an asynchronous sibling, whereas the Abort method does not. The reason stems from the different roles of the Close and Abort methods. For example, in the graceful shutdown initiated by invoking the Close (or BeginClose) method, the CommunicationObject can perform I/O while shutting down the object. To illustrate, consider the case of calling Close during a WS-ReliableMessaging (WS-RM) choreography. In this case, the Close method will cause the channel responsible for WS-RM to send a TerminateSequence message to the other participant. In other words, the Close method can trigger I/O.

On the other hand, the immediate shutdown initiated by invoking the Abort method will immediately shut down the CommunicationObject and will perform minimal I/O. As a result, there is no need for an asynchronous sibling to the Abort method. It is also worth mentioning that the Abort method does not accept a TimeSpan as a parameter, while the Close method does.

The collaboration pattern between the CommunicationObject and the CommunicationObject-derived type that occurs as a result of invoking the Close or BeginClose method is very similar to the collaboration pattern that occurs as a result of invoking the Open method. As shown earlier, invoking the CommunicationObject.Open method can lead to an invocation of the OnOpening, OnOpen, and OnOpened methods. Likewise, invoking the CommunicationObject.Close method can cause the OnClosing, OnClose, and OnClosed methods to execute. The following code snippet illustrates the way the .NET Framework implements the CommunicationObject.Close method:

 public void Close(TimeSpan timeout){   // only general implementation shown   this.OnClosing();   this.OnClose(timeout);   this.OnClosed(); }

Furthermore, the CommunicationObject raises the Closing and Closed events in a manner similar to the way it raises the Opening and Opened events.

The Abort method starts a different sort of collaboration. The following code snippet illustrates the way the .NET Framework implements the CommunicationObject.Abort method:

 public void Abort(){   // only general implementation shown   this.OnClosing();   this.OnAbort(); // only difference from Close   this.OnClosed(); }

As the preceding code snippet shows, the Abort method invokes methods that are also in the normal execution path of the Close method. The OnClosing and OnClosed methods raise the Closing and Closed events, respectively. In effect, the Abort method shares some of the execution path of the Close method and raises the same events as the Close method.

Remembering that one of the primary jobs of the CommunicationObject type is to maintain a consistent state machine, it stands to reason that the execution paths of the Close and Abort methods change based on the State property of the object being closed or aborted. To illustrate, consider the case of calling Close when the state is CommunicationState.Created. If the Open method has not been called, should there be any difference in execution paths between Close and Abort? Remember that the real work of initializing the CommunicationObject results from calling the Open or BeginOpen method. Until one of these methods executes, the CommunicationObject is nothing more than an object on the heap. In the pre-open state, the CommunicationObject.Close method and CommunicationObject.Abort method perform the same work. However, after the Open or BeginOpen method executes, the CommunicationObject might have a reference to something like a connected socket, and the CommunicationObject. Close and CommunicationObject.Abort methods perform very different work. Table 6-1 describes how the state of the CommunicationObject impacts the way Close and Abort execute. As you review this table, remember that Close is the graceful way to tear down a CommunicationObject and Abort is the abrupt way to tear down a CommunicationObject.

Table 6-1: CommunicationState, Close, and Abort
Open table as spreadsheet

State Property

Close

Abort

CommunicationState.Created

Calls Abort

Aborts normally

CommunicationState.Opening

Calls Abort

Aborts normally

CommunicationState.Opened

Closes normally

Aborts normally

CommunicationState.Closing

No action

Aborts normally

CommunicationState.Closed

No action

No action

The Fault Method

The protected Fault method is a way for a CommunicationObject to shut down, but it is not part of the ICommunicationObject interface. Because it is not visible to outside callers, the Fault method is a way for a CommunicationObject-derived type to sense an error condition and abruptly shut down the channel. Calling the Fault method transitions the State property to CommunicationState.Faulted and invokes the OnFaulted virtual method, thereby allowing a CommunicationObject-derived type to define its own behavior. In most cases, the OnFaulted method calls the Abort method.

About CommunicationObject Stacks

Remember that the CommunicationObject type is the base type for all channels and channel factories. Remember also that channels and channel factories are commonly arranged as a stack, and only the top of the stack is visible to a caller. In concept, this sort of arrangement is possible via a type such as the following:

 internal sealed class MyCommunicationObject : CommunicationObject {   private CommunicationObject _inner;   internal MyCommunicationObject(CommunicationObject inner){     this._inner = inner;   }   // other implementation omitted for brevity }

Because MyCommunicationObject derives from CommunicationObject, it is subject to the state machine defined in CommunicationObject. Furthermore, MyCommunicationObject has the responsibility of synchronizing its transition through the state machine with the _inner member variable’s transition through the state machine. For example, if a referent of a MyCommunicationObject object calls the Open method, the MyCommunicationObject.Open implementation must also call the Open method on its inner member variable, as shown here:

 internal sealed class MyCommunicationObject : CommunicationObject {   private CommunicationObject _inner;   internal MyCommunicationObject(CommunicationObject inner){     this._inner = inner;   }   protected override void OnOpen(TimeSpan timeout) {      // MyCommunicationObject.OnOpen implementation here      // ...      // Call Open on the inner member variable      // NOTE: may want to reduce timeout      _inner.Open(timeout);    }    // other implementation omitted for brevity }

When arranged in this way, the referent that calls MyCommunicationObject.Open does not have to know all of the CommunicationObject nodes in the stack, and they all transition through the same state machine in a synchronized manner. For thoroughness, it is important to note that it does not matter whether the call to _inner.Open occurs before or after the MyCommunicationObject.OnOpen method. In practice, it is usually performed at the end of the method. It might be necessary to adjust the TimeSpan passed to the inner member variable to reflect the remaining time allowed in the operation.




Inside Windows Communication Foundation
Inside Windows Communication Foundation (Pro Developer)
ISBN: 0735623066
EAN: 2147483647
Year: 2007
Pages: 106
Authors: Justin Smith

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net