Chapter 8 Rules' Testing and Troubleshooting
Abstract
使用drools最困难地方就是处理规则的错误和异常。
如果一个knowledge base中有多个规则,这个问题将更加难处理。
drools声明式的特性天生就给测试代码带来一定的挑战,不像Java的类和函数可以直接调用。如果想要测试某一个规则,我们需要重新创建必要的session状态,这个状态必须满足该规则的when条件,还有就是相同的状态可能触发同一个knowledge base中的其他规则,这就要求我们在测试一个knowledge base时,需要将涉及到的所有规则的各种场景都遍历测试一遍。
在drools中,测试和调试规则没有什么灵丹妙药,我们能做的就是总结好的实践,技巧,以及一些小贴士(tips)
KIE Base partition(分类) and testing
Rules' left-hand side troubleshooting
Rules' right-hand side troubleshooting
KIE Base partition and testing
Create loosely coupled DRLs
create tightly-integrated, loosely-coupled assets(高内聚,低耦合)
分类:
- 按规则的输入(比如,所有规则都是和Customers有关的)
- 按规则的主题(比如,risk evaluation rules,customer scoring rules)
- 其他根据我们的业务确定的有意义的一些分类
如果模块比较独立或者和其他模块不是紧耦合,那么可以给它们创建不同的knowledge base。独立的knowledge base不仅使我们的模块更容易测试,而且系统也更加灵活,执行效率也会越高。Kie Base中规则越少,测试越简单,触发一些不可预期的规则的机会也越小
Prefer KieHelper over a KieContainer classpath
写测试的时候,最好不要用KieContainer classpath(KieServices.Factory.get().getKieClasspathContainer()),因为它会扫描整个classpath下的META-INF/kmodule.xml文件。如果我们只测试一个单一规则或者classpath下的某些规则,那么我们只需创建一个相对较小的containers,它只包含你测试场景所用到的一些资源。最简单的方法就是使用 org.kie.internal.utils.KieHelper类,利用它,我们可以创建一个只包含一些特定资源的KieContainer,此外他还可以用来校验加载的资源是否合法
KieHelper kieHelper = new KieHelper(); kieHelper.addResource(ResourceFactory.newClassPathResource("some/file.drl"), ResourceType.DRL);
//add more resources if needed
Results results = kieHelper.verify();
if (results.hasMessages(Message.Level.WARNING, Message.Level.ERROR)){
//fail
}
KieBase kieBase = kieHelper.build()
kieHelper还有addContent, addFromClassPath().当然,KieHelper不仅可以用于单元测试和集成测试,应用中也可以使用该类细粒度控制kie base中包含的资源。
Benefits of using globals
global 变量不仅可以用来在规则中与外部服务交互,而且可以在测试中发挥作用。
global AuditService auditService;
rule "Send Suspicious Operation to Audit Service"
when
$so: SuspiciousOperation()
then
auditService.notifySuspiciousOperation($so);
end
使用global来引入auditService,比起在规则的then语句中初始化,减少了复杂度,而且如果借助依赖注入(如:CDI等)将会使得测试更加容易。
Rules' left-hand side troubleshooting
Debugging the left-hand side of a rule
在Kie Base所有的组件中(规则,函数,global等),最难调试和问题定位的是规则的left-hand
不像规则的右边,所有规则的左边都被编译成一个网络节点,在网络中节点的类型与规则的关系不是很明确,尽管网络节点很明显提高了规则的执行效率,但是也加重了错误和异常的定位难度。但是后学的一些技巧和小贴士可供参考。
Left-hand side troubleshooting
规则左边大致有三类问题:
- 编译错误
- 运行时错误
- 规则本没有按预期被触发
Compilation errors,编译错误
规则左边的编译错误很容易发现,当Kie Base中的相应资源没有被加载起来时,就是编译错误。
注:编译错误时KieContainer不会报任何错误,而只是返回一个空的KIE Base,这就是为什么在使用KieContainer时要使用verify来验证它。
编译错误有一下几类:
- 语法错误,drl本身不合法
- 使用为注册的组件。比如,自定义的accumulate函数或操作符
- model相关错误。比如,不存在的类,属性等
- 没有或错误导入类(import)
无论哪一种编译错误,当KieContainer被verify时都会打印详细的信息。
rule "Send Suspicious Operation to Audit Service"
when
$so: SuspiciousOperationXXX()
then
auditService.notifySuspiciousOperation($so);
end
verify打印的错误信息:
[ERROR] - chapter05/globals-5/globals-5.drl[28,0]: Unable to resolve ObjectType 'SuspiciousOperationXXX'
[ERROR] - chapter05/globals-5/globals-5.drl[26,0]: Rule Compilation error $so cannot be resolved to a variable
行号和列号都是指向最后生成的drl文件的。
运行时错误
drools中的运行时错误是非常危险的,它可能会使kiesession处在一个不可靠的状态,因为drools不是事务的,插入,修改,删除过程中出现该错误可能导致一些规则超预期的触发。所以测试我们的规则是非常必要的。
减少甚至避免此类错误最好的方式就是尽可能的覆盖所有ksession可能的场景
引起运行时错误最常见的原因:
- model类抛出异常。包括model类的getter和普通方法
- 组件抛出的异常。比如,函数,自定义accumulate函数,自定义操作符等
- drools本身的bug
通常情况,drools中抛出的运行时错误提供了堆栈信息,包括两个重要的异常: 主异常,定位异常抛出的地方;根异常,指出错误的原因。
rule "Detect suspicious amount operations"
when
$c: Customer()
Number( doubleValue > amountThreshold ) from accumulate (Order ( state != OrderState.COMPLETED, $total: total) from orderService.getOrdersByCustomer($c.customerId),sum($total)
)
then
insert(new SuspiciousOperation($c,SuspiciousOperation.Type.SUSPICIOUS_AMOUNT)
);
end
[Error: orderService.getOrdersByCustomer($c.customerId): No connection to host]
[Near : {... orderService.getOrdersByCustom ....}]
^
[Line: 1, Column: 1]
at o.m.o.i.r.ReflectiveAccessorOptimizer.compileGetChain(ReflectiveAccessorOptimizer.java:435)
… 55 more
Caused by: java.lang.reflect.InvocationTargetException
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
... 61 more
Caused by: java.lang.IllegalStateException: No connection to host at org.drools.devguide.chapter05.GlobalsTest$3.
getOrdersByCustomer(GlobalsTest.java:263)
Rules not being triggered
99%的情况与下面这三种原因有关:
- 规则没有按照我们认为的那样写,误解了规则的意图
- 插入的facts不是我们想要的
- kie base中不包含我们想要的资源
剩下的1%可能是drools本身的bug导致。
还有一种情况比较难定位,关于from关键字的
rule "Detect suspicious discount operations"
when
$c: Customer()
$o: Order(customer == $c)
Discount(percentage > 90.0) from $o.discount
then
insert(new SuspiciousOperation($c,SuspiciousOperation.Type.SUSPICIOUS_DISCOUNT)
);
end
上面的例子是正确的,可以匹配,下面这个不会匹配但是也不会报错,需要特别注意。
rule "Detect suspicious discount operations"
when
$c: Customer()
$o: Order(customer == $c)
Item(id > 90.0) from $o.discount
then
insert(new SuspiciousOperation($c,SuspiciousOperation.Type.SUSPICIOUS_DISCOUNT));
end
the usage
of heterogeneous collections in the right-hand side of a from keyword without generating unnecessary errors.
drools 中from关键字对待集合(collection)的方式也很特别,需要注意
SuspiciousOperationService.java
public interface SuspiciousOperationService {
public Collection<SuspiciousOperation> getSuspiciousOperationsByCustomer(Long customerId);
}
rule
rule "Low category of customers with suspicious operations"
when
$c: Customer(category == Category.GOLD)
Collection(size > 3) from suspiciousOperationService.getSuspiciousOperationsByCustomer($c.customerId)
then
modify($c){setCategory(Category.SILVER)};
end
当from右边是集合时,drools默认会循环集合中的元素,来评估集合中的每一个元素
rule "Low category of GOLD customers with suspicious operations"
when
$c: Customer(category == Category.GOLD)
List(size > 3) from collect (SuspiciousOperation() from suspiciousOperationService.getSuspiciousOperationsByCustomer($c.customerId))
then
modify($c){setCategory(Category.SILVER)};
end
通过collect关键可以解决这种问题,from左边就不会再循环每一个元素了。
Event listeners
有两个listener对定位drools问题有帮助:RuleRuntimeEventListener和AgendaEventListener
RuleRuntimeEventListener的作用是,当一个fact被插入,更新或者移除时,在规则的右边发出一些通知;AgendaEventListener用来监视规则左边的条件什么时候匹配,规则右边什么时候被fire。
同一个Kie Base中的规则之间的冲突也会导致规则不被触发
rule "Low category of GOLD customers with suspicious operations"
when
$c: Customer(category == Category.GOLD)
List(size > 3) from collect (SuspiciousOperation(customer ==$c))
then
modify($c){setCategory(Category.SILVER)};
end
同一个Kie Base的另一个规则:
rule "Categorize Customers between 22 and 30"
when
$c: Customer(age > 21, age < 31, category !=Category.BRONZE)
then
modify($c){setCategory(Category.BRONZE)};
end
假如有一个金牌会员有三次违规操作,但也符合大于21岁小于31岁,如果第二个规则先触发,那么一个规则将不会被触发。
Event Listener可以帮助我们确定问题出在哪儿,档处理冲突规则时,RuleRuntimeEventListener和AgendaEventListener配合使用,我们就可以判断当一个fact被什么时候被插入,修改,什么时候条件匹配成功。
drools中有两个已经实现的类:
org.kie.api.
event.rule.DebugRuleRuntimeEventListener 和org.kie.api.event.rule.DebugAgendaEventListener。
==>[ObjectInsertedEventImpl: [object: [Customer [id = 1, age=24,category = GOLD]]]
==>[ObjectInsertedEventImpl: [object: [SuspiciousOperation[customer=Customer [id = 1, age=24,category = GOLD], type=SUSPICIOUS_AMOUNT]]]
==>[ObjectInsertedEventImpl: [object: [SuspiciousOperation[customer=Customer [id = 1, age=24,category = GOLD], type=SUSPICIOUS_AMOUNT]]]
==>[ObjectInsertedEventImpl: [object: [SuspiciousOperation[customer=Customer [id = 1, age=24,category = GOLD], type=SUSPICIOUS_AMOUNT]]]
==>[ObjectInsertedEventImpl: [object: [SuspiciousOperation[customer=Customer [id = 1, age=24,category = GOLD], type=SUSPICIOUS_AMOUNT]]]
==>[ObjectInsertedEventImpl: [object: [SuspiciousOperation[customer=Customer [id = 1, age=24,category = GOLD], type=SUSPICIOUS_AMOUNT]]]
==>[ActivationCreatedEvent: rule: [Categorize Customers between 22 and30]]
==>[ObjectUpdatedEventImpl: [object: [Customer [id =1,age=24,category = BRONZE]]]
==>[AfterActivationFiredEvent: rule: [Categorize Customers between 22and 30]]
ObjectInsertedEventImpl,ObjectUpdatedEventImpl是DebugRuleRuntimeEventListener 监控的内容,而ActivationCreatedEvent,AfterActivationFiredEvent是DebugAgendaEventListener监控的内容。
drools logs
drools内部使用的日志框架是slf4j,它是一个门面,你可以使用实现它的任何框架,比如:logback,log4j,Commons logging等。好的实践是日志级别可以先设置成TRACE,然后根据打印的多少结合自身的业务来逐渐收窄日志级别。
Create simpler versions of a rule
测试规则时,假如有一个测试失败了(比如是规则没有被触发),前面介绍的技巧都失效了,那么还有一个方法就是为了测试方便,把这个规则尝试转换成条件更少的更贱但的规则,逐步查看是否触发,最后找到那个不触发规则的条件或者说原因,最后问题就落到我们前面介绍的哪几种情况了:
- 规则不是按照们的意图所写
- 插入的fact不是我们想要的
所以我们尽量要把大的规则拆分成小的规则,不仅仅是为了容易理解,对测试,定位问题也有好处。
Making our rules simple is not just something we want to do only for testing
purposes. If we have a rule with too many patterns, it is a good practice to split it
into multiple rules, each one inserting a specific object to mark the rule as fired.
Later, another small rule can group those marked objects and trigger the main
consequence of our really complex rule. This would not only allow us to increase the
performance of the session when having many objects, but also to create warning
rules, which check whether only some of the marked objects are present and not the
rest and take compensatory actions, such as sending a warning or invoking some
external service to retrieve more data from a global variable.
Debugging the right-hand side of a rule
规则右边主要有下面三种元素组成:
- Java 语句
- MVEL表达式
- drools中预定义的变量和方法。如,变量:drools,kcontext;方法:insert,update,modify,delete等
不像规则左边编译时被转换成一个网络节点,规则右边被编译成一个Java类,指向这个类的引用被添加到terminal node。
虽然可以用断点的方式来调试规则右边,但这也仅仅局限于非常严格的环境,所以需要一些好的实践和技巧来使得规则右边更容易调试。
eclipse plugin: http://docs.jboss.org/drools/release/latestFinal/drools-docs/html/ch01.html#d0e368
Right-hand side troubleshooting
好消息是规则左边被转换成Java类,对于我们来说定位Java类的故障是比较熟悉的,主要有两类错误:编译错误和运行时错误
Compilation errors
和规则左边相同,drools对编译错误不会被Kie Base本身管理(就是本身不打印编译错误信息),而是由verify来打印
rule "Send Suspicious Operation to Audit Service"
when
$so: SuspiciousOperation()
then
auditServiceXXX.notifySuspiciousOperation($so);
end
打印的错误信息:
[ERROR] - chapter05/globals-5/globals-5.drl[26,0]: Rule Compilation error auditServiceXXX cannot be resolved
Runtime errors
运行时错误会引起ksession不稳定和不可恢复的状态,所以需要加大测试场景覆盖率
**Exception executing consequence for rule "Send Suspicious Operation to Audit Service" in chapter05.globals5: java.lang.IllegalStateException:Unable to contact Audit Service: No route to host.**
at o.d.c.r.r.i.DefaultConsequenceExceptionHandler.handleException(
DefaultConsequenceExceptionHandler.java:39)
at o.d.c.c.DefaultAgenda.fireActivation(DefaultAgenda.java:1083)
... 37 more
Caused by: **java.lang.IllegalStateException: Unable to contact Audit Service: No route to host.**
at org.drools.devguide.chapter05.GlobalsTest$4.notifySuspiciousOpe
ration(GlobalsTest.java:286)
at chapter05.globals5.Rule_Send_Suspicious_Operation_to_Audit_
Service1050594099.defaultConsequence(Rule_Send_Suspicious_Operation_
to_Audit_Service1050594099.java:7)
... 40 more
Right-hand side good practices
规则左边的好的实践也是适用于规则右边的,
- 用global变量引用服务
- 用event listener
- 创建更简单的Right-hand side
- 使用log框架
Dumping the generated Java classes
drools可以将转化后的class输出到dump文件,可以指定输出到文件系统中,这样可以更好的理解转换后的Java代码的具体执行过程,他们也可以设置断点来进行调试
drools提供两种方式将产生的class文件输出到一个文件系统中
- 通过系统变量 (mvn test -Ddrools.dump.dir="/tmp/classes")
- 在看module.xml文件中声明
<kmodule>
<configuration>
<property key="drools.dump.dir" value="/tmp/classes"/>
</configuration>
...
</kmodule>
Reporting a bug in Drools
Drools' issues are tracked in the following web application: https://issues.jboss.org/projects/DROOLS
GitHub repository at https://github.com/droolsjbpm/drools.