一起谈.NET技术,基于CallContextInitializer的WCF扩展导致的严重问题
2011-08-29 19:26
543 查看
WCF是一个具有极高扩展度的分布式通信框架,无论是在信道层(ChannelLayer)还是服务模型层(ServiceModel),我们都可以自定义相关组件通过相应的扩展注入到WCF运行环境中。在WCF众多可扩展点中,ICallContextInitializer可以帮助我们在服务操作执行前后完成一些额外的功能,这实际上就是一种AOP的实现方式。比如在《通过WCFExtension实现Localization》中,我通过ICallContextInitializer确保了服务操作具有和客户端一样的语言文化;在《通过WCFExtension实现Context信息的传递》中,我通过ICallContextInitializer实现上下文在客户端到服务端的自动传递。ICallContextInitializer的定义如下:
[/code]
昨天,李永京同学问了我一个相关的问题。问题大概是这样的,他采用ICallContextInitializer实现WCF与NHibernate的集成。具体来说是通过ICallContextInitializer实现对事务的提交,即通过BeforeInvoke方法初始化NHibernate的Session,通过AfterInvoke提交事务。但是,这中间具有一个挺严重的问题:当执行AfterInvoke提交事务的时候,是可能抛出异常的。一旦异常从AfterInvoke抛出,整个服务端都将崩溃。我们现在就来讨论一下这个问题,以及问题产生的根源。
这里下载该例子。首先我定义了如下一个实现了ICallContextInitializer接口的自定义CallContextInitializer:MyCallContextInitializer。在AfterInvoke方法中,我直接抛出一个异常。
然后,我们通过ServiceBehavior的方式来应用上面定义的MyCallContextInitializer。为此,我们定义了如下一个实现了IServiceBehavior接口的服务行为:MyServiceBehaviorAttribute。在ApplyDispatchBehavior方法中,将我们自定义的MyCallContextInitializer对象添加到所有终结点的分发运行时操作的CallContextInitializer列表中。
然后,我们采用我们熟悉的计算服务的例子来验证MyCallContextInitializer对整个服务端运行时的影响。下面是服务契约和服务类型的定义,我们自定义的服务行为MyServiceBehaviorAttribute通过自定义特性的方式应用到CalculatorService上面。
后然我们通过Console应用的方式来Host上面定义的CalculatorService,并创建另一个Console应用来模拟客户端对服务进行调用。由于相应的实现比较简单,在这里就不写出来了,对此不清楚的读者可以直接下载例子查看源代码。当你运行程序的时候,作为宿主的Console应用会崩溃,相应的进程也会被终止。如果服务宿主程序正常终止,客户端会抛出如左图所示的一个CommunicationException异常。
如果在调用超时时限内,服务宿主程序没能正常终止,客户端则会抛出如右图所示的TimeoutException异常。
查看EventLog,你会发现两个相关的日志。它们的Source分别是:System.ServiceMode3.0.0.0和.NETRuntime。两条日志相应的内容如下。如果你足够细心,你还会从中看到WCF一个小小的BUG。日志内容的第二行为“Message:ICallContextInitializer.BeforeInvokethrewanexceptionoftypeSystem.Exception:调用MyCallContextInitializer.AfterInvoke()出错!”,实际上这里的“ICallContextInitializer.BeforeInvoke”应该改成“ICallContextInitializer.AfterInvoke”。下面一部分中你将会看到这个BUG是如何产生的。
如果你想从消息交换得角度进一步剖析问题的本质,你可以采用Fiddler这样的工具。如果你真的这样做的话,你会发现服务端没有任何消息返回到客户端。
ICallContextInitializer的AfterInvoke方法的最终是通过定义在DispatchOperationRuntime类型的一个命名为UninitializeCallContextCore的私有方法中被调用的。下面就是该方法的定义:
通过上面的代码,你会看到对DispatchOperation所有CallContextInitializer的AfterInvoke方法的调用是放在一个Try/Catch中进行的。当异常抛出后,会调用DiagnosticUtility的FailFast方法。传入该方法的是异常消息,你可以看到这里指定的消息是不对的,“ICallContextInitializer.BeforeInvoke”应该是“ICallContextInitializer.AfterInvoke”,这就是为什么你在EventLog看到日志内容是不准确的真正原因。我们进一步来看看FailFast的定义:
从上面的代码可以看到,整个过程分为两个步骤:对消息尽心Trace后调用Environment.FailFast方法。对Environment.FailFast方法具有一定了解的人应该之后,该方法执行后会终止掉当前进程。这就是为什么在ICallContextInitializer的AfterInvoke方法执行过程中出现未处理异常会导致宿主程序的非正常崩溃的真正原因。
[code]publicinterfaceICallContextInitializer
{
//Methods
voidAfterInvoke(objectcorrelationState);
objectBeforeInvoke(InstanceContextinstanceContext,IClientChannelchannel,Messagemessage);
}
[/code]
昨天,
一、问题重现
为了重现这个问题,我写了一个很简单的例子,你可以从publicclassMyCallContextInitializer:ICallContextInitializer
{
publicvoidAfterInvoke(objectcorrelationState)
{
thrownewException("调用MyCallContextInitializer.AfterInvoke()出错!");
}
publicobjectBeforeInvoke(InstanceContextinstanceContext,IClientChannelchannel,Messagemessage)
{
returnnull;
}
}
然后,我们通过ServiceBehavior的方式来应用上面定义的MyCallContextInitializer。为此,我们定义了如下一个实现了
publicclassMyServiceBehaviorAttribute:Attribute,IServiceBehavior
{
publicvoidAddBindingParameters(ServiceDescriptionserviceDescription,ServiceHostBaseserviceHostBase,Collection<ServiceEndpoint>endpoints,BindingParameterCollectionbindingParameters){}
publicvoidApplyDispatchBehavior(ServiceDescriptionserviceDescription,ServiceHostBaseserviceHostBase)
{
foreach(ChannelDispatcherdispatcherinserviceHostBase.ChannelDispatchers)
{
foreach(EndpointDispatcherendpointindispatcher.Endpoints)
{
foreach(DispatchOperationoperationinendpoint.DispatchRuntime.Operations)
{
operation.CallContextInitializers.Add(newMyCallContextInitializer());
}
}
}
}
publicvoidValidate(ServiceDescriptionserviceDescription,ServiceHostBaseserviceHostBase){}
}
然后,我们采用我们熟悉的计算服务的例子来验证MyCallContextInitializer对整个服务端运行时的影响。下面是服务契约和服务类型的定义,我们自定义的服务行为MyServiceBehaviorAttribute通过自定义特性的方式应用到CalculatorService上面。
namespaceArtech.Exception2CallContextInitializer.Contracts
{
[ServiceContract(Namespace="http://www.artech.com/")]
publicinterfaceICalculator
{
[OperationContract]
doubleAdd(doublex,doubley);
}
}
namespaceArtech.Exception2CallContextInitializer.Services
{
[MyServiceBehavior]
publicclassCalculatorService:ICalculator
{
publicdoubleAdd(doublex,doubley)
{
returnx+y;
}
}
}
后然我们通过Console应用的方式来Host上面定义的CalculatorService,并创建另一个Console应用来模拟客户端对服务进行调用。由于相应的实现比较简单,在这里就不写出来了,对此不清楚的读者可以直接下载例子查看源代码。当你运行程序的时候,作为宿主的Console应用会崩溃,相应的进程也会被终止。如果服务宿主程序正常终止,客户端会抛出如左图所示的一个CommunicationException异常。
如果在调用超时时限内,服务宿主程序没能正常终止,客户端则会抛出如右图所示的TimeoutException异常。
查看EventLog,你会发现两个相关的日志。它们的Source分别是:System.ServiceMode3.0.0.0和.NETRuntime。两条日志相应的内容如下。如果你足够细心,你还会从中看到WCF一个小小的BUG。日志内容的第二行为“Message:ICallContextInitializer.BeforeInvokethrewanexceptionoftypeSystem.Exception:调用MyCallContextInitializer.AfterInvoke()出错!”,实际上这里的“ICallContextInitializer.BeforeInvoke”应该改成“ICallContextInitializer.AfterInvoke”。下面一部分中你将会看到这个BUG是如何产生的。
FailFastwasinvoked.
Message:ICallContextInitializer.BeforeInvokethrewanexceptionoftypeSystem.Exception:调用MyCallContextInitializer.AfterInvoke()出错!
StackTrace:atSystem.ServiceModel.Diagnostics.ExceptionUtility.TraceFailFast(Stringmessage,EventLoggerlogger)
atSystem.ServiceModel.Diagnostics.ExceptionUtility.TraceFailFast(Stringmessage)
atSystem.ServiceModel.DiagnosticUtility.FailFast(Stringmessage)
atSystem.ServiceModel.Dispatcher.DispatchOperationRuntime.UninitializeCallContextCore(MessageRpc&rpc)
atSystem.ServiceModel.Dispatcher.DispatchOperationRuntime.UninitializeCallContext(MessageRpc&rpc)
atSystem.ServiceModel.Dispatcher.DispatchOperationRuntime.InvokeBegin(MessageRpc&rpc)
atSystem.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage5(MessageRpc&rpc)
atSystem.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage4(MessageRpc&rpc)
atSystem.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage3(MessageRpc&rpc)
atSystem.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage2(MessageRpc&rpc)
atSystem.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage1(MessageRpc&rpc)
atSystem.ServiceModel.Dispatcher.MessageRpc.Process(BooleanisOperationContextSet)
atSystem.ServiceModel.Dispatcher.ImmutableDispatchRuntime.Dispatch(MessageRpc&rpc,BooleanisOperationContextSet)
atSystem.ServiceModel.Dispatcher.ChannelHandler.DispatchAndReleasePump(RequestContextrequest,BooleancleanThread,OperationContextcurrentOperationContext)
atSystem.ServiceModel.Dispatcher.ChannelHandler.HandleRequest(RequestContextrequest,OperationContextcurrentOperationContext)
atSystem.ServiceModel.Dispatcher.ChannelHandler.AsyncMessagePump(IAsyncResultresult)
atSystem.ServiceModel.Dispatcher.ChannelHandler.OnAsyncReceiveComplete(IAsyncResultresult)
atSystem.ServiceModel.Diagnostics.Utility.AsyncThunk.UnhandledExceptionFrame(IAsyncResultresult)
atSystem.ServiceModel.AsyncResult.Complete(BooleancompletedSynchronously)
atSystem.ServiceModel.Channels.InputQueue`1.AsyncQueueReader.Set(Itemitem)
atSystem.ServiceModel.Channels.InputQueue`1.EnqueueAndDispatch(Itemitem,BooleancanDispatchOnThisThread)
atSystem.ServiceModel.Channels.InputQueue`1.EnqueueAndDispatch(Titem,ItemDequeuedCallbackdequeuedCallback,BooleancanDispatchOnThisThread)
atSystem.ServiceModel.Channels.InputQueueChannel`1.EnqueueAndDispatch(TDisposableitem,ItemDequeuedCallbackdequeuedCallback,BooleancanDispatchOnThisThread)
atSystem.ServiceModel.Channels.SingletonChannelAcceptor`3.Enqueue(QueueItemTypeitem,ItemDequeuedCallbackdequeuedCallback,BooleancanDispatchOnThisThread)
atSystem.ServiceModel.Channels.HttpChannelListener.HttpContextReceived(HttpRequestContextcontext,ItemDequeuedCallbackcallback)
atSystem.ServiceModel.Channels.SharedHttpTransportManager.OnGetContextCore(IAsyncResultresult)
atSystem.ServiceModel.Channels.SharedHttpTransportManager.OnGetContext(IAsyncResultresult)
atSystem.ServiceModel.Diagnostics.Utility.AsyncThunk.UnhandledExceptionFrame(IAsyncResultresult)
atSystem.Net.LazyAsyncResult.Complete(IntPtruserToken)
atSystem.Net.LazyAsyncResult.ProtectedInvokeCallback(Objectresult,IntPtruserToken)
atSystem.Net.ListenerAsyncResult.WaitCallback(UInt32errorCode,UInt32numBytes,NativeOverlapped*nativeOverlapped)
atSystem.Threading._IOCompletionCallback.PerformIOCompletionCallback(UInt32errorCode,UInt32numBytes,NativeOverlapped*pOVERLAP)
ProcessName:Artech.Exception2CallContextInitializer.Services
ProcessID:7652
.NETRuntimeversion2.0.50727.4927-ICallContextInitializer.BeforeInvokethrewanexceptionoftypeSystem.Exception:调用MyCallContextInitializer.AfterInvoke()出错!
如果你想从消息交换得角度进一步剖析问题的本质,你可以采用Fiddler这样的工具。如果你真的这样做的话,你会发现服务端没有任何消息返回到客户端。
二、原因剖析
从上面表现出来的现象,我们可以知道这是一个非常严重的问题,因为它将会终止整个服务宿主进程。那么,是什么导致了这个严重的问题呢?实际上,如果通过Reflector对WCF相关代码进行反射,你将会很容易找到问题的根源。privatevoidUninitializeCallContextCore(refMessageRpcrpc)
{
objectproxy=rpc.Channel.Proxy;
intcallContextCorrelationOffset=this.Parent.CallContextCorrelationOffset;
try
{
for(inti=this.CallContextInitializers.Length-1;i>=0;i--)
{
this.CallContextInitializers[i].AfterInvoke(rpc.Correlation[callContextCorrelationOffset+i]);
}
}
catch(Exceptionexception)
{
DiagnosticUtility.FailFast(string.Format(CultureInfo.InvariantCulture,"ICallContextInitializer.BeforeInvokethrewanexceptionoftype{0}:{1}",newobject[]{exception.GetType(),exception.Message}));
}
}
通过上面的代码,你会看到对
[MethodImpl(MethodImplOptions.NoInlining)]
internalstaticExceptionFailFast(stringmessage)
{
try
{
try
{
ExceptionUtility.TraceFailFast(message);
}
finally
{
Environment.FailFast(message);
}
}
catch
{
}
Environment.FailFast(message);
returnnull;
}
从上面的代码可以看到,整个过程分为两个步骤:对消息尽心Trace后调用Environment.FailFast方法。对Environment.FailFast方法具有一定了解的人应该之后,该方法执行后会终止掉当前进程。这就是为什么在ICallContextInitializer的AfterInvoke方法执行过程中出现未处理异常会导致宿主程序的非正常崩溃的真正原因。
三、总结
CallContextInitializer的设计可以看成是AOP在WCF中的实现,它可以在服务操作执行前后对方法调用进行拦截。你可以通过自定义CallContextInitializer实现一些服务操作执行前的初始化操作,以及操作执行后的清理工作。但是,当你自定义CallContextInitializer的时候,一定要确保AfterInvoke方法中没有异常抛出来。相关文章推荐
- 基于CallContextInitializer的WCF扩展导致的严重问题
- 跟着Artech学习WCF扩展(3) 扩展CallContextInitializer
- 一起谈.NET技术,WCF的问题和Using语句块
- 一起谈.NET技术,基于Visual Studio 2010 阐述C#4个特性
- 一起谈.NET技术,NHibernate3.0剖析:Query篇之NHibernate.Linq自定义扩展
- 一起谈.NET技术,浅析ASP.NET清空缓存时遇到的问题
- [WCF]由于签名问题,运行某些基于 WCF 的项目模板将导致与 svcutil.exe 崩溃
- 一起谈.NET技术,如何解决分布式系统中的跨时区问题[实例篇]
- 一起谈.NET技术,使用WCF实现SOA面向服务编程——简单的WCF开发实例
- 一起谈.NET技术,Silverlight同步(Synchronous)调用WCF服务
- 一起谈.NET技术,ASP.NET 4.0 一些隐性的扩展
- 一起谈.NET技术,.NET4.0的可扩展缓存系统
- 一起谈.NET技术,Siverlight与WCF通信之双工netTcp实现视频对话
- call to OpenGL ES API with no current context (logged once per thread)可能会导致加载问题?
- 一起谈.NET技术,从扩展方法到流畅的程序体验(一)
- 一起谈.NET技术,.NET 4的新特性:图表、SEO及可扩展的输出缓存
- 一起谈.NET技术,使用LINQ to SQL更新数据库(上):问题重重
- 一起谈.NET技术,详细述说ADO超时相关问题介绍
- 一起谈.NET技术,详解ASP.NET页面的aspx扩展
- 一起谈.NET技术,ASP.NET应用下基于SessionState的“状态编程框架”解决方案