一起谈.NET技术,基于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="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异常。

 

  如果在调用超时时限内,服务宿主程序没能正常终止,客户端则会抛出如右图所示的TimeoutException异常。

  查看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
  本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

时间: 2024-10-16 21:42:34

一起谈.NET技术,基于CallContextInitializer的WCF扩展导致的严重问题的相关文章

基于CallContextInitializer的WCF扩展导致的严重问题

WCF是一个具有极高扩展度的分布式通信框架,无论是在信道层(Channel Layer)还是服务模型层(Service Model),我们都可以自定义相关组件通过相应的扩展注入到WCF运行环境中.在WCF众多可扩展点中,ICallContextInitializer可以帮助我们在服务操作执行前后完成一些额外的功能,这实际上就是一种AOP的实现方式.比如在<通过WCF Extension实现Localization>中,我通过ICallContextInitializer确保了服务操作具有和客户

一起谈.NET技术,Siverlight与WCF通信之双工netTcp实现视频对话

效果 先看看效果再说,基本逻辑是两个人通过Silverlight端,借助TCP协议分别向服务器不断传输视频,服务器接收到视频后,会检测这些视频是发给谁的,然后回调某个客户端来接收并显示这些视频. 实现 双工的服务契约定义: [ServiceContract(CallbackContract=typeof(IChatServiceCallBack))]public interface IChatService { [OperationContract]void SendVideo(UserVide

一起谈.NET技术,不错的VS2010扩展——JSEnhancements,让js和css也折叠

在Visaul Studio 2010中写js或css代码,缺少像写C#代码时的那种折叠功能,当代码比较多时,就很不方便. 今天发现,已经有VS2010扩展支持这个功能,它就是--JSEnhancements(下载地址). 用了一下,感觉不错,定义region,只需将#region写在注释中即可.请看下面的演示: 先看JavaScript 未使用JSEnhancements的情况: 1. 使用JSEnhancements之后(未定义region): 折起 展开 2. 使用JSEnhancemen

基于WF与WCF构建数据逻辑层

WF是什么,许多对NET技术有了解的人能说出一点,但又说不清楚 不论你认为WF是什么,但不要与Jbpm ,Shark ,Biztalk,SharePoint 这些产品做比效,这些产品有共同的特点就是面向企业业务流程应用的产品,WF不是,WF面向的开发人员 WF是一个使用XML描述,具有IOC.AOP功能的面向流程控制的开发平台. 我从事工作流开发有8年了,学习WF已经有5年了,在博客园写关于WF的主题博客也快4年了,自从接触WF后我一直在解释WF与传统工作流之间的区别 可能是我即从事工作流开发,

Geneva框架:构建基于声明的WCF服务的更好方法

本文基于"Geneva"框架的预发布版本撰写而成.所有信息均有可能发生变更. 本文使用以下技术: Windows Communication Foundation "Geneva"框架(以前称为"Zermatt")是用于构建基于声明的应用程序和服务以及实现联合安全方案的新框架代号.它的功能包括用于构建自定义安全令牌服务 (STS) 的探测功能.要求从 ASP.NET 应用程序进行联合身份验证的机制,以及简化 ASP.NET 应用程序和 Windo

一起谈.NET技术,Azure和Bing Maps API示例经验分享

头疼的Bug,糟糕的代码,崩溃的调试作为开发人员的你,遇到上述任何一种情况可能就会陷入抓狂.如果能直接获得需要的代码,编程的活儿就会轻松许多. 微软最新推出的一站式示例代码库,让开发人员可以免费获得所需的示例代码或向微软工程师提出示例请求,轻松解决常见的编程问题,大大减轻工作负担. 本文以一个名为AzureBingMaps的示例应用程序为例,分享了一些在开发该示例过程中积累的经验,以期对广大开发人员有所帮助.AzureBingMaps是一个旅游站点管理系统,演示了很多技术,可以认为是一个实际项目

《创业家》牛文文:少谈点模式多谈点技术

"模式"如同当年的"主义",流行于各种创业大赛.创业励志节目.论坛的"街头"式秀场 文/创业家 牛文文 "美国某某公司你知道吧?就是刚被戴尔.惠普.思科十几亿美元抢购的那家.我们的模式和它的一样,现在还没赢利,可将来起码有十几亿人民币的市值." "我开了小煤矿,但煤运不出去,上商学院之后受到启发,想搞模式创新,具体讲就是想在铁路边上搞个煤炭物流开发区,建一个大的物流和信息流平台,把分散的煤炭集中在我这个园区,这样和铁

一起谈.NET技术,最全的ASP.NET开源CMS汇总

国内: 1.SiteServer CMS SiteServer CMS 网站内容管理系统(著作权登记号2008SR15710)是定位于中高端市场的CMS内容管理系统,能最近汇总了一些asp.net开源cms,希望对学习ASP.NET的人员带来帮助: 国内CMS: 1.SiteServer CMS SiteServer CMS 网站内容管理系统(著作权登记号2008SR15710)是定位于中高端市场的CMS内容管理系统,能够以最低的成本.最少的人力投入在最短的时间内架设一个功能齐全.性能优异.规模

一起谈.NET技术,如何解决分布式系统中的跨时区问题[原理篇]

一.场景以及需求   为了让大家本文介绍的主题有一个比较直观的认识,我们给出一个具体的应用场景.一个跨国公司开发一套统一的办公系统,供遍布全球的所有分公司使用.客户端的UI采用Smart Client (Windows Forms应用),而主要的业务逻辑均通过WCF服务的形式提供.我们将承载业务服务的服务器成为应用服务器,如右图(点击看大图)所示,应用服务器部属于中国境内(东8区).主要的客户端(分公司)分布于三个主要的国家和地区:北美.欧州和澳洲. 不论客户端和服务器之间,还是不同的客户端之间