基于CallContextInitializer的WCF擴展導致的嚴重問題

作者: Artech  來源: 博客園  發布時間: 2010-07-29 15:47  閱讀: 839 次  推薦: 0   原文鏈接   [收藏]  

  WCF是一個具有極高擴展度的分布式通信框架,無論是在信道層(Channel Layer)還是服務模型層(Service Model),我們都可以自定義相關組件通過相應的擴展注入到WCF運行環境中。在WCF眾多可擴展點中ICallContextInitializer可以幫助我們在服務操作執行前后完成一些額外的功能,這實際上就是一種AOP的實現方式。比如在《通過WCF Extension實現Localization》中,我通過ICallContextInitializer確保了服務操作具有和客戶端一樣的語言文化;在《通過WCF Extension實現Context信息的傳遞》中,我通過ICallContextInitializer實現上下文在客戶端到服務端的自動傳遞。ICallContextInitializer的定義如下:

   1: public interface ICallContextInitializer
   2: {
   3:     // Methods
   4:     void AfterInvoke(object correlationState);
   5:     object BeforeInvoke(InstanceContext instanceContext, IClientChannel channel, Message message);
   6: }

  昨天,李永京同學問了我一個相關的問題。問題大概是這樣的,他采用ICallContextInitializer實現WCF與NHibernate的集成。具體來說是通過ICallContextInitializer實現對事務 的提交,即通過BeforeInvoke方法初始化NHibernate的Session,通過AfterInvoke提交事務。但是,這中間具有一個挺嚴重的問題:當執行AfterInvoke提交事務的時候,是可能拋出異常的。一旦異常從AfterInvoke拋出,整個服務端都將崩潰。我們現在就來討論一下這個問題,以及問題產生的根源。

  一、問題重現

  為了重現這個問題,我寫了一個很簡單的例子,你可以從這里下載該例子。首先我定義了如下一個實現了ICallContextInitializer接口的自定義CallContextInitializer:MyCallContextInitializer。在AfterInvoke方法中,我直接拋出一個異常。

   1: public class MyCallContextInitializer : ICallContextInitializer
   2: {
   3:     public void AfterInvoke(object correlationState)
   4:     {
   5:         throw new Exception("調用MyCallContextInitializer.AfterInvoke()出錯!");
   6:     }
   7:  
   8:     public object BeforeInvoke(InstanceContext instanceContext, IClientChannel channel, Message message)
   9:     {
  10:         return null;
  11:     }
  12: }

  然后,我們通過ServiceBehavior的方式來應用上面定義的MyCallContextInitializer。為此,我們定義了如下一個實現了IServiceBehavior接口的服務行為:MyServiceBehaviorAttribute。在ApplyDispatchBehavior方法中,將我們自定義的MyCallContextInitializer對象添加到所有終結點的分發運行時操作的CallContextInitializer列表中。

   1: public class MyServiceBehaviorAttribute : Attribute, IServiceBehavior
   2: {
   3:     public void AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, Collection<ServiceEndpoint> endpoints, BindingParameterCollection bindingParameters) { }
   4:  
   5:     public void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
   6:     {
   7:         foreach (ChannelDispatcher dispatcher in serviceHostBase.ChannelDispatchers)
   8:         {
   9:             foreach (EndpointDispatcher endpoint in dispatcher.Endpoints)
  10:             {
  11:                 foreach (DispatchOperation operation in endpoint.DispatchRuntime.Operations)
  12:                 {
  13:                     operation.CallContextInitializers.Add(new MyCallContextInitializer());
  14:                 }
  15:             }
  16:         }
  17:     }
  18:  
  19:     public void Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase) { }
  20: }

  然后,我們采用我們熟悉的計算服務的例子來驗證MyCallContextInitializer對整個服務端運行時的影響。下面是服務契約和服務類型的定義,我們自定義的服務行為MyServiceBehaviorAttribute通過自定義特性的方式應用到CalculatorService上面。

   1: namespace Artech.Exception2CallContextInitializer.Contracts
   2: {
   3:     [ServiceContract(Namespace="http://www.artech.com/")]
   4:     public interface ICalculator
   5:     {
   6:         [OperationContract]
   7:         double Add(double x, double y);
   8:     }
   9: }
   1: namespace Artech.Exception2CallContextInitializer.Services
   2: {
   3:     [MyServiceBehavior]
   4:     public class CalculatorService:ICalculator
   5:     {
   6:         public double Add(double x, double y)
   7:         {
   8:             return x + y;
   9:         }
  10:     }
  11: }

  后然我們通過Console應用的方式來Host上面定義的CalculatorService,并創建另一個Console應用來模擬客戶端對服務進行調用。由于相應的實現比較簡單,在這里就不寫出來了,對此不清楚的讀者可以直接下載例子查看源代碼。當你運行程序的時候,作為宿主的Console應用會崩潰,相應的進程也會被終止。如果服務宿主程序正常終止,客戶端會拋出如左圖所示的一個CommunicationException異常。

image

 

  如果在調用超時時限內,服務宿主程序沒能正常終止,客戶端則會拋出如右圖所示的TimeoutException異常。

image  查看Event Log,你會發現兩個相關的日志。它們的Source分別是:System.ServiceMode 3.0.0.0和.NET Runtime。兩條日志相應的內容如下。如果你足夠細心,你還會從中看到WCF一個小小的BUG。日志內容的第二行為“Message: ICallContextInitializer.BeforeInvoke threw an exception of type System.Exception: 調用MyCallContextInitializer.AfterInvoke()出錯!”,實際上這里的“ICallContextInitializer.BeforeInvoke”應該改成“ICallContextInitializer.AfterInvoke”。下面一部分中你將會看到這個BUG是如何產生的。

FailFast was invoked.
 Message: ICallContextInitializer.BeforeInvoke threw an exception of type System.Exception: 調用MyCallContextInitializer.AfterInvoke()出錯!
 Stack Trace:    at System.ServiceModel.Diagnostics.ExceptionUtility.TraceFailFast(String message, EventLogger logger)
   at System.ServiceModel.Diagnostics.ExceptionUtility.TraceFailFast(String message)
   at System.ServiceModel.DiagnosticUtility.FailFast(String message)
   at System.ServiceModel.Dispatcher.DispatchOperationRuntime.UninitializeCallContextCore(MessageRpc& rpc)
   at System.ServiceModel.Dispatcher.DispatchOperationRuntime.UninitializeCallContext(MessageRpc& rpc)
   at System.ServiceModel.Dispatcher.DispatchOperationRuntime.InvokeBegin(MessageRpc& rpc)
   at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage5(MessageRpc& rpc)
   at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage4(MessageRpc& rpc)
   at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage3(MessageRpc& rpc)
   at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage2(MessageRpc& rpc)
   at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage1(MessageRpc& rpc)
   at System.ServiceModel.Dispatcher.MessageRpc.Process(Boolean isOperationContextSet)
   at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.Dispatch(MessageRpc& rpc, Boolean isOperationContextSet)
   at System.ServiceModel.Dispatcher.ChannelHandler.DispatchAndReleasePump(RequestContext request, Boolean cleanThread, OperationContext currentOperationContext)
   at System.ServiceModel.Dispatcher.ChannelHandler.HandleRequest(RequestContext request, OperationContext currentOperationContext)
   at System.ServiceModel.Dispatcher.ChannelHandler.AsyncMessagePump(IAsyncResult result)
   at System.ServiceModel.Dispatcher.ChannelHandler.OnAsyncReceiveComplete(IAsyncResult result)
   at System.ServiceModel.Diagnostics.Utility.AsyncThunk.UnhandledExceptionFrame(IAsyncResult result)
   at System.ServiceModel.AsyncResult.Complete(Boolean completedSynchronously)
   at System.ServiceModel.Channels.InputQueue`1.AsyncQueueReader.Set(Item item)
   at System.ServiceModel.Channels.InputQueue`1.EnqueueAndDispatch(Item item, Boolean canDispatchOnThisThread)
   at System.ServiceModel.Channels.InputQueue`1.EnqueueAndDispatch(T item, ItemDequeuedCallback dequeuedCallback, Boolean canDispatchOnThisThread)
   at System.ServiceModel.Channels.InputQueueChannel`1.EnqueueAndDispatch(TDisposable item, ItemDequeuedCallback dequeuedCallback, Boolean canDispatchOnThisThread)
   at System.ServiceModel.Channels.SingletonChannelAcceptor`3.Enqueue(QueueItemType item, ItemDequeuedCallback dequeuedCallback, Boolean canDispatchOnThisThread)
   at System.ServiceModel.Channels.HttpChannelListener.HttpContextReceived(HttpRequestContext context, ItemDequeuedCallback callback)
   at System.ServiceModel.Channels.SharedHttpTransportManager.OnGetContextCore(IAsyncResult result)
   at System.ServiceModel.Channels.SharedHttpTransportManager.OnGetContext(IAsyncResult result)
   at System.ServiceModel.Diagnostics.Utility.AsyncThunk.UnhandledExceptionFrame(IAsyncResult result)
   at System.Net.LazyAsyncResult.Complete(IntPtr userToken)
   at System.Net.LazyAsyncResult.ProtectedInvokeCallback(Object result, IntPtr userToken)
   at System.Net.ListenerAsyncResult.WaitCallback(UInt32 errorCode, UInt32 numBytes, NativeOverlapped* nativeOverlapped)
   at System.Threading._IOCompletionCallback.PerformIOCompletionCallback(UInt32 errorCode, UInt32 numBytes, NativeOverlapped* pOVERLAP)
 
 Process Name: Artech.Exception2CallContextInitializer.Services
 Process ID: 7652
  .NET Runtime version 2.0.50727.4927 - ICallContextInitializer.BeforeInvoke threw an exception of type System.Exception: 調用MyCallContextInitializer.AfterInvoke()出錯!

  如果你想從消息交換得角度進一步剖析問題的本質,你可以采用Fiddler這樣的工具。如果你真的這樣做的話,你會發現服務端沒有任何消息返回到客戶端。

  二、原因剖析

  從上面表現出來的現象,我們可以知道這是一個非常嚴重的問題,因為它將會終止整個服務宿主進程。那么,是什么導致了這個嚴重的問題呢?實際上,如果通過Reflector對WCF相關代碼進行反射,你將會很容易找到問題的根源。

  ICallContextInitializer的AfterInvoke方法的最終是通過定義在DispatchOperationRuntime類型的一個命名為UninitializeCallContextCore的私有方法中被調用的。下面就是該方法的定義:

   1: private void UninitializeCallContextCore(ref MessageRpc rpc)
   2: {
   3:     object proxy = rpc.Channel.Proxy;
   4:     int callContextCorrelationOffset = this.Parent.CallContextCorrelationOffset;
   5:     try
   6:     {
   7:         for (int i = this.CallContextInitializers.Length - 1; i >= 0; i--)
   8:         {
   9:             this.CallContextInitializers[i].AfterInvoke(rpc.Correlation[callContextCorrelationOffset + i]);
  10:         }
  11:     }
  12:     catch (Exception exception)
  13:     {
  14:         DiagnosticUtility.FailFast(string.Format(CultureInfo.InvariantCulture, "ICallContextInitializer.BeforeInvoke threw an exception of type {0}: {1}", new object[] { exception.GetType(), exception.Message }));
  15:     }
  16: }
  17:  
  18:  
  19:  
  20:  

  通過上面的代碼,你會看到對DispatchOperation所有CallContextInitializer的AfterInvoke方法的調用是放在一個Try/Catch中進行的。當異常拋出后,會調用DiagnosticUtility的FailFast方法。傳入該方法的是異常消息,你可以看到這里指定的消息是不對的,“ICallContextInitializer.BeforeInvoke”應該是“ICallContextInitializer.AfterInvoke”,這就是為什么你在Event Log看到日志內容是不準確的真正原因。我們進一步來看看FailFast的定義:

   1: [MethodImpl(MethodImplOptions.NoInlining)]
   2: internal static Exception FailFast(string message)
   3: {
   4:     try
   5:     {
   6:         try
   7:         {
   8:             ExceptionUtility.TraceFailFast(message);
   9:         }
  10:         finally
  11:         {
  12:             Environment.FailFast(message);
  13:         }
  14:     }
  15:     catch
  16:     {
  17:     }
  18:     Environment.FailFast(message);
  19:     return null;
  20: }

  從上面的代碼可以看到,整個過程分為兩個步驟:對消息盡心Trace后調用Environment.FailFast方法。對Environment.FailFast方法具有一定了解的人應該之后,該方法執行后會終止掉當前進程。這就是為什么在ICallContextInitializer的AfterInvoke方法執行過程中出現未處理異常會導致宿主程序的非正常崩潰的真正原因。

  三、總結

  CallContextInitializer的設計可以看成是AOP在WCF中的實現,它可以在服務操作執行前后對方法調用進行攔截。你可以通過自定義CallContextInitializer實現一些服務操作執行前的初始化操作,以及操作執行后的清理工作。但是,當你自定義CallContextInitializer的時候,一定要確保AfterInvoke方法中沒有異常拋出來。

  作者:Artech
  出處:http://artech.cnblogs.com
  本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。
0
0
 
標簽:WCF
 
 

文章列表

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()