前段时间写了一篇《深入biztalk中Delivery Notification和ACK、NACK机制》(后面成此文为“前文”),深入的探讨了发送消息的反馈机制,但是没有提供详细的示例。有朋友看了文章后自己做了测试,发现有些问题跟文章中介绍的有些不一致,我自己也发现对Delivery Notification机制有些地方的认识还有模糊的地方,现在继续就这个Delivery Notification展开进一步的深入探究,这回全部的探讨全部建立在测试实例的基础之上,做一次比较透彻的实验。
测试的目的:
l 在前文中提到,发送端口的Delivery Notification属性设置为 Transmitted,orchestration中发送形状发送消息后,orchestration应该在收到返回的ACK或NACK消息之前,停留在发送形状处等待,直到收到ACK或NACK消息为止。但是,我在实际使用过中发现,在atomic的scope中发送消息后,orchestration并没有在发送形状处等待反馈消息,而是继续按流程走下去了。这个测试需要明确在各种scope类型(None,Atomic,Long Running),各种synchronized属性设置下,orchestration是否在发送形状处等待ACK/NACK消息的返回。
l 有网友反映,只有在物理发送端口的高级传输选项中的重试次数和重试间隔不设为0才能捕获到异常,设为0就捕获不到。这个测试也需要明确发送端口出错后,在什么条件下可以捕获到DeliveryFailureException异常,捕获后如何处理。
本文的源代码下载:/Files/chnking/Samples.rar
一. 测试案例设计
设计一个很简单的场景,有个用户表,字段有:
Id – 用户的id,主键
Name – 用户名
流程是这样的,biztalk从目录名为“in”文件夹中读取新增用户的xml消息,类似这样的形式:
<ns0:Person xmlns:ns0="http://ACK_Sample.person">
<id>1</id>
<name>name_1</name>
</ns0:Person>
新增用户通过Port_InputPerson端口进入到Orchestration。
之后把新增用户消息通过映射转换成插入到sql server数据库的updategram消息,通过Port_InsertPerson端口把新增用户的数据发送到sql server数据库,数据库引擎把用户数据插入到用户表中。
给这个scope增加一个Exception Handler(atomic scope没有Exception Handler,这种情况后面会探讨),捕获的Exception类型为Microsoft.XLANGs.BaseTypes.DeliveryFailureException。
正常情况,用户id没有跟用户表中已有用户重复,此用户顺利的插入到用户表
失败的情况,用户id在用户表中已经存在,数据库引擎返回主键重复的错误,orchestration应该能收到NACK消息,产生DeliveryFailureException异常,并被Exception Handler捕获。
流程如下:
设计中在orchestration的一些关键点设置了表达式形状,在orchestration运行到这些点的时候,表达式往日志中写入表示已执行到这一点的信息。使用类似下面这样的语句:
System.Diagnostics.EventLog.WriteEntry("用户信息插入到Sql之前","Ack_Sample");
从上图中可以看到,在四个关键点设置了四个表达式,记录到日志的信息分别是:
Before_sendSql:用户信息插入到Sql之前
After_sendSql:用户信息插入到Sql之后
Enter_Exception:捕获到DeliveryFailureException异常
Out_Scope:流程运行出Scope范围
二. 测试过程
发送端口Port_InsertPerson的Delivery Notification属性始终设置为 Transmitted,保证sql 适配器返回ACK或者NACK消息。
Orchestration本身的事务类型设置为long running,因为测试中orchestration中需要包含各种事务类型的scope,所以orchestration的事务类型必须设为long running。
测试主要针对Scope事务类型、Scope的synchronized属性、物理发送端口是否设置重试这三种属性的不同设置对ACK或NACK的返回、DeliveryFailureException异常的产生和orchestration的相关行为的影响。
开始分几种情况测试
1. 场景一:Scope事务类型None,Scope的synchronized属性false,物理发送端口重试3次
首先新增id为“1”,用户名为“name_1”的用户,将这个xml文件放入到in目录中,稍后,在日志中可以看到这样的信息:
到数据库中查看用户表,id为“1”,用户名为“name_1”的用户已成功的插入到用户表中。
然后,还是插入这个id为“1”的用户,明显的,这次的插入不会成功,因为违反了主键不能重复的约束。运行后,结果如下:
从上图可以看到以下一些有趣的事实:
在“用户信息插入Sql之前”事件后马上就是“用户信息插入Sql之后”事件,之后才出现错误事件,对照前面的流程图,可以看出发送形状发送消息后orchestration并不是在发送形状处等待,而是这个scope的结束的地方等待ACK或NACK的消息。
从图中的“捕获到DeliveryFailureException异常”事件也能看出,在sql adapetr出错,并重试了三次后,的确发回了NACK的消息,并有orchestration生成DeliveryFailureException异常抛出,这个异常也被Exception Handler捕获到了。
下面改变一下Scope的synchronized属性为true测试
2. 场景二:Scope事务类型None,Scope的synchronized属性true,物理发送端口重试3次
测试结果跟场景一完全一样。
3. 场景三:Scope事务类型None,Scope的synchronized属性true,物理发送端口重试0次
插入重复的用户信息,测试结果如下:
设置物理发送端口的重试次数为0后,看测试结果,除了发送端口没有尝试重新发送消息外,其它的结果跟场景一都一样,sql适配器发生错误后,在orchestration能捕获到DeliveryFailureException异常。
4. 场景四:Scope事务类型None,Scope的synchronized属性false,物理发送端口重试0次
测试结果跟场景三完全一样。
鉴于物理发送端口的重试次数的设置跟是否返回ACK或者NACK消息没有关系,下面的测试都把重试次数设置为0,以加快测试速度。
5. 场景五:Scope事务类型Long Running,Scope的synchronized属性false
测试结果跟场景三完全一样。
6. 场景六:Scope事务类型Long Running,Scope的synchronized属性true
测试结果跟场景三完全一样。
阶段性结论:
在scope的事务类型为none或long running时,scope的Synchronized的设置对scope中的发送端口的ACK或NACK的返回没有任何影响。
orchestration等待发送端口返回ACK或者NACK消息总是在发送端口所在的scope的结束处等待。
物理发送端口的重试次数的设置跟是否返回ACK或者NACK消息没有关系。
下面要进行scope的事务类型为atomic时的测试,这里有个问题,atomic的scope没有Exception Handler,测试流程需要做一些改造。
7. 场景七:Scope事务类型Atomic
Atomic类型的Scope,总是被认为是Synchronized,所有Atomicde Synchronized属性不起作用,如果Synchronized还是被设置为ture,编译的时候编译器会提示:the 'synchronized' keyword has no effect on atomic scopes。
测试流程做一个变通,把发送形状用一个事务类型为none的scope包围起来,非事务的scope可以有Exception Handler,在这个嵌套scope的Exception Handler中设计捕获DeliveryFailureException异常。
再测试看消息从发送形状发送出去后,orchestration是否还是停留none事务的那个scope结束处等待ACK或NACK的返回。Exception Handler能否捕获到DeliveryFailureException异常。
改造后的流程如下图:
测试前,期望的效果是:消息从发送形状发送出去后,orchestration在Scope_2的结束处等待ACK或NACK的消息返回。如果返回的是ACK消息,流程正常走下去,如果返回的是NACK消息,orchestration收到后抛出DeliveryFailureException异常,异常被Scope_2的Exception Handler捕获到。
测试,将重复的用户消息放入到in文件夹,稍后,观察事件查看器,如下:
这里看到一些很奇怪的现象:
l 消息从发送形状发出后,没有在嵌套的scope的结束处暂停等待ACK或NACK消息,也没有在外部的Atomic类型的scope的结束处暂定,直接的运行出了scope的范围。这说明一点:Atomic类型的scope中,orchestration不会在实例订阅的订阅点等待消息的进入,而是直接运行下去。
l 在事件中没有看到捕获到DeliveryFailureException异常。在事件查看器中可以看到在orchestration中运行出scope范围后,有个警告事件,这是物理发送端口报告插入sql是有主键重复错误,然后第一个错误事件是提示发送端口服务被挂起,最后的那个错误是:“Uncaught exception (see the 'inner exception' below) has suspended an instance of service 'ACK_Sample.ACK,Exceptions.Types.DeliveryFailureException。”表示orchestration收到了NACK消息并产生了DeliveryFailureException异常,但是抛出的DeliveryFailureException异常在抛出的范围内未被Exception Handler捕获,或者捕获后又被继续抛出。
在这个场景下,可以看到两个被挂起的服务实例,一个是发送端口实例,因为发送到sql server的消息出错,发送端口被挂起。第二个是orchestration,因为有个未被捕获的异常DeliveryFailureException,所以orchestration也被挂起。
Atomic事务是否已被成功提交
从上面的结果,我们也会产生一个怀疑,Atomic类型的scope没有等待ACK或NACK的返回直接就走下去了,是不是流程出了Atomic scope后,Atomic的事务就算成功的提交了呢?
为了验证这一点,在out_Scope表达式形状中,增加下面这样的语句:
if (succeeded (Transaction_2) == true)
{
System.Diagnostics.EventLog.WriteEntry("Atomic事务提交完成!","Ack_Sample");
}
else
{
System.Diagnostics.EventLog.WriteEntry("Atomic事务未提交完成!","Ack_Sample");
}
表达式succeeded (事务的identifier)可以判断一个scope的事务是否成功的提交。
依然输入重复主键的用户消息,再看事件日志:
可以看到日志中有个“Atomic事务提交完成!”事件,证实了之前的猜测,说明流程一旦出了atomic的scope,atomic的事务即被提交。
显然,这样的结果不是我们想要的,因为在一个long running的scope嵌套atomic的scope的情况下,涉及到atomic事务的补偿问题。如果一个atomic scope内部实际上出错了,但是事务却已被成功提交,在整个long running事务需要回滚时,成功的atomic的事务会调用这个atomic scope的补偿块中的代码进行补偿,这样很可能出问题,可能对没有成功的操作做逆向操作。关于事务补偿的问题以后在专门讨论事务的文章总详细探讨。
更改测试方案
上面的测试方案不是我们预期的效果,更改一下测试方案,吧atomic内部的none类型的scope去掉,然后在atomic scope外部套一个long running的scope,用这个L-R的scope是不是能不能捕获到DeliveryFailureException
输入重复主键的用户消息,再看事件日志:
从这个测试的事件日志来看,有几点可以总结:
l orchestration会在含有需要返回ACK或者NACK的atomic中的直接外层的long running scope的结束处等待ACK或者NACK的返回。
l NACK的产生的DeliveryFailureException异常可以被atomic的直接外层的L-R scope捕获。