閱讀214 返回首頁    go 阿裏雲 go 技術社區[雲棲]


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

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="https://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方法中沒有異常拋出來。


作者:蔣金楠
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
原文鏈接

最後更新:2017-10-27 14:34:19

  上一篇:go  你知道Unity IoC Container是如何創建對象的嗎?
  下一篇:go  如何讓普通變量也支持事務回滾?