NIFI-6854 - Added option to ignore condition errors in rules. Test correction

NIFI-6854 - change warning to debug

Signed-off-by: Matthew Burgess <mattyb149@apache.org>

This closes #3876
This commit is contained in:
Yolanda M. Davis 2019-11-07 17:21:48 -05:00 committed by Matthew Burgess
parent c998a7259a
commit a37b57e96b
No known key found for this signature in database
GPG Key ID: 05D3DEB8126DAD24
13 changed files with 312 additions and 17 deletions

View File

@ -79,6 +79,7 @@
<exclude>src/test/resources/test_mvel_rules.json</exclude>
<exclude>src/test/resources/test_mvel_rules.yml</exclude>
<exclude>src/test/resources/test_spel_rules.json</exclude>
<exclude>src/test/resources/test_bad_spel_rules.json</exclude>
<exclude>src/test/resources/test_spel_rules.yml</exclude>
</excludes>
</configuration>

View File

@ -0,0 +1,54 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.nifi.rules;
import org.jeasy.rules.api.Condition;
import org.jeasy.rules.api.Facts;
import org.jeasy.rules.mvel.MVELCondition;
import org.mvel2.MVEL;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Serializable;
public class RulesMVELCondition implements Condition {
private static final Logger LOGGER = LoggerFactory.getLogger(MVELCondition.class);
private String expression;
private Serializable compiledExpression;
private boolean ignoreConditionErrors;
public RulesMVELCondition(String expression, boolean ignoreConditionErrors) {
this.expression = expression;
this.compiledExpression = MVEL.compileExpression(expression);
this.ignoreConditionErrors = ignoreConditionErrors;
}
public boolean evaluate(Facts facts) {
try {
return (Boolean)MVEL.executeExpression(this.compiledExpression, facts.asMap());
} catch (Exception ex) {
if(ignoreConditionErrors) {
LOGGER.debug("Unable to evaluate expression: '" + this.expression + "' on facts: " + facts, ex);
return false;
} else{
throw ex;
}
}
}
}

View File

@ -0,0 +1,75 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.nifi.rules;
import org.jeasy.rules.api.Condition;
import org.jeasy.rules.api.Facts;
import org.jeasy.rules.spel.SpELCondition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.ParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
public class RulesSPELCondition implements Condition {
private static final Logger LOGGER = LoggerFactory.getLogger(SpELCondition.class);
private final ExpressionParser parser = new SpelExpressionParser();
private String expression;
private Expression compiledExpression;
private boolean ignoreConditionErrors;
public RulesSPELCondition(String expression) {
this.expression = expression;
this.compiledExpression = this.parser.parseExpression(expression);
}
public RulesSPELCondition(String expression, ParserContext parserContext) {
this.expression = expression;
this.compiledExpression = this.parser.parseExpression(expression, parserContext);
}
public RulesSPELCondition(String expression, boolean ignoreConditionErrors) {
this.expression = expression;
this.compiledExpression = this.parser.parseExpression(expression);
this.ignoreConditionErrors = ignoreConditionErrors;
}
public RulesSPELCondition(String expression, ParserContext parserContext, boolean ignoreConditionErrors) {
this.expression = expression;
this.compiledExpression = this.parser.parseExpression(expression, parserContext);
this.ignoreConditionErrors = ignoreConditionErrors;
}
public boolean evaluate(Facts facts) {
try {
StandardEvaluationContext context = new StandardEvaluationContext();
context.setRootObject(facts.asMap());
context.setVariables(facts.asMap());
return this.compiledExpression.getValue(context, Boolean.class);
} catch (Exception ex) {
if(ignoreConditionErrors) {
LOGGER.debug("Unable to evaluate expression: '" + this.expression + "' on facts: " + facts, ex);
return false;
} else{
throw ex;
}
}
}
}

View File

@ -31,14 +31,14 @@ import org.apache.nifi.rules.Action;
import org.apache.nifi.rules.ActionHandler;
import org.apache.nifi.rules.Rule;
import org.apache.nifi.rules.RulesFactory;
import org.apache.nifi.rules.RulesMVELCondition;
import org.apache.nifi.rules.RulesSPELCondition;
import org.jeasy.rules.api.Condition;
import org.jeasy.rules.api.Facts;
import org.jeasy.rules.api.RuleListener;
import org.jeasy.rules.api.Rules;
import org.jeasy.rules.core.DefaultRulesEngine;
import org.jeasy.rules.core.RuleBuilder;
import org.jeasy.rules.mvel.MVELCondition;
import org.jeasy.rules.spel.SpELCondition;
import java.util.ArrayList;
import java.util.Collections;
@ -89,10 +89,21 @@ public class EasyRulesEngineService extends AbstractControllerService implement
.defaultValue(NIFI.getValue())
.build();
static final PropertyDescriptor IGNORE_CONDITION_ERRORS = new PropertyDescriptor.Builder()
.name("rules-ignore-condition-errors")
.displayName("Ignore Condition Errors")
.description("When set to true, rules engine will ignore errors for any rule that encounters issues " +
"when compiling rule conditions (including syntax errors and/or missing facts). Rule will simply return as false " +
"and engine will continue with execution.")
.required(true)
.defaultValue("false")
.allowableValues("true", "false")
.build();
protected List<PropertyDescriptor> properties;
protected volatile List<Rule> rules;
protected volatile String rulesFileFormat;
private boolean ignoreConditionErrors;
@Override
protected void init(ControllerServiceInitializationContext config) throws InitializationException {
@ -101,6 +112,7 @@ public class EasyRulesEngineService extends AbstractControllerService implement
properties.add(RULES_FILE_TYPE);
properties.add(RULES_FILE_PATH);
properties.add(RULES_FILE_FORMAT);
properties.add(IGNORE_CONDITION_ERRORS);
this.properties = Collections.unmodifiableList(properties);
}
@ -114,6 +126,7 @@ public class EasyRulesEngineService extends AbstractControllerService implement
final String rulesFile = context.getProperty(RULES_FILE_PATH).getValue();
final String rulesFileType = context.getProperty(RULES_FILE_TYPE).getValue();
rulesFileFormat = context.getProperty(RULES_FILE_FORMAT).getValue();
ignoreConditionErrors = context.getProperty(IGNORE_CONDITION_ERRORS).asBoolean();
try{
rules = RulesFactory.createRules(rulesFile, rulesFileType, rulesFileFormat);
} catch (Exception fex){
@ -129,7 +142,7 @@ public class EasyRulesEngineService extends AbstractControllerService implement
@Override
public List<Action> fireRules(Map<String, Object> facts) {
final List<Action> actions = new ArrayList<>();
if(rules == null){
if (rules == null || facts == null || facts.isEmpty()) {
return null;
}else {
org.jeasy.rules.api.Rules easyRules = convertToEasyRules(rules, (action, eventFacts) ->
@ -149,7 +162,7 @@ public class EasyRulesEngineService extends AbstractControllerService implement
rules.forEach(rule -> {
RuleBuilder ruleBuilder = new RuleBuilder();
Condition condition = rulesFileFormat.equalsIgnoreCase(SPEL.getValue())
? new SpELCondition(rule.getCondition()): new MVELCondition(rule.getCondition());
? new RulesSPELCondition(rule.getCondition(), ignoreConditionErrors): new RulesMVELCondition(rule.getCondition(), ignoreConditionErrors);
ruleBuilder.name(rule.getName())
.description(rule.getDescription())
.priority(rule.getPriority())

View File

@ -128,7 +128,7 @@ public class TestRulesFactory {
&& rule1.getPriority() == 1 && rule1.getCondition().equals("predictedQueuedCount > 50");
checkDiagnostic = rule2.getName().equals("Time To Back Pressure") && rule2.getDescription().equals("Back pressure time less than 5 minutes")
&& rule2.getPriority() == 2 && rule2.getCondition().equals("predictedTimeToBytesBackpressureMillis >= 300000") && checkDiagnostic;
&& rule2.getPriority() == 2 && rule2.getCondition().equals("predictedTimeToBytesBackpressureMillis < 300000 && predictedTimeToBytesBackpressureMillis >= 0") && checkDiagnostic;
return checkDiagnostic;

View File

@ -21,6 +21,7 @@ import org.apache.nifi.rules.Action;
import org.apache.nifi.util.TestRunner;
import org.apache.nifi.util.TestRunners;
import org.junit.Test;
import org.mvel2.PropertyAccessException;
import java.io.IOException;
import java.util.HashMap;
@ -29,6 +30,7 @@ import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
public class TestEasyRulesEngineService {
@ -44,7 +46,7 @@ public class TestEasyRulesEngineService {
runner.assertValid(service);
Map<String, Object> facts = new HashMap<>();
facts.put("predictedQueuedCount",60);
facts.put("predictedTimeToBytesBackpressureMillis",300000);
facts.put("predictedTimeToBytesBackpressureMillis",299999);
List<Action> actions = service.fireRules(facts);
assertNotNull(actions);
assertEquals(actions.size(), 3);
@ -62,7 +64,7 @@ public class TestEasyRulesEngineService {
runner.assertValid(service);
Map<String, Object> facts = new HashMap<>();
facts.put("predictedQueuedCount",60);
facts.put("predictedTimeToBytesBackpressureMillis",300000);
facts.put("predictedTimeToBytesBackpressureMillis",299999);
List<Action> actions = service.fireRules(facts);
assertNotNull(actions);
assertEquals(actions.size(), 2);
@ -80,7 +82,7 @@ public class TestEasyRulesEngineService {
runner.assertValid(service);
Map<String, Object> facts = new HashMap<>();
facts.put("predictedQueuedCount",60);
facts.put("predictedTimeToBytesBackpressureMillis",300000);
facts.put("predictedTimeToBytesBackpressureMillis",299999);
List<Action> actions = service.fireRules(facts);
assertNotNull(actions);
assertEquals(actions.size(), 2);
@ -98,7 +100,7 @@ public class TestEasyRulesEngineService {
runner.assertValid(service);
Map<String, Object> facts = new HashMap<>();
facts.put("predictedQueuedCount",60);
facts.put("predictedTimeToBytesBackpressureMillis",300000);
facts.put("predictedTimeToBytesBackpressureMillis",299999);
List<Action> actions = service.fireRules(facts);
assertNotNull(actions);
assertEquals(actions.size(), 2);
@ -116,7 +118,7 @@ public class TestEasyRulesEngineService {
runner.assertValid(service);
Map<String, Object> facts = new HashMap<>();
facts.put("predictedQueuedCount",60);
facts.put("predictedTimeToBytesBackpressureMillis",300000);
facts.put("predictedTimeToBytesBackpressureMillis",299999);
List<Action> actions = service.fireRules(facts);
assertNotNull(actions);
assertEquals(actions.size(), 2);
@ -134,12 +136,146 @@ public class TestEasyRulesEngineService {
runner.assertValid(service);
Map<String, Object> facts = new HashMap<>();
facts.put("predictedQueuedCount",60);
facts.put("predictedTimeToBytesBackpressureMillis",300000);
facts.put("predictedTimeToBytesBackpressureMillis",299999);
List<Action> actions = service.fireRules(facts);
assertNotNull(actions);
assertEquals(actions.size(), 2);
}
@Test
public void testIgnoreConditionErrorsFalseNIFI() throws InitializationException, IOException {
final TestRunner runner = TestRunners.newTestRunner(TestProcessor.class);
final RulesEngineService service = new MockEasyRulesEngineService();
runner.addControllerService("easy-rules-engine-service-test",service);
runner.setProperty(service, EasyRulesEngineService.RULES_FILE_PATH, "src/test/resources/test_nifi_rules.json");
runner.setProperty(service,EasyRulesEngineService.RULES_FILE_TYPE, "JSON");
runner.setProperty(service,EasyRulesEngineService.RULES_FILE_FORMAT, "NIFI");
runner.setProperty(service,EasyRulesEngineService.IGNORE_CONDITION_ERRORS,"false");
runner.enableControllerService(service);
runner.assertValid(service);
Map<String, Object> facts = new HashMap<>();
facts.put("fakeMetric",60);
facts.put("predictedTimeToBytesBackpressureMillis",299999);
try {
service.fireRules(facts);
fail("Expected exception to be thrown");
}catch (PropertyAccessException pae){
assert true;
}
}
@Test
public void testIgnoreConditionErrorsTrueNIFI() throws InitializationException, IOException {
final TestRunner runner = TestRunners.newTestRunner(TestProcessor.class);
final RulesEngineService service = new MockEasyRulesEngineService();
runner.addControllerService("easy-rules-engine-service-test",service);
runner.setProperty(service, EasyRulesEngineService.RULES_FILE_PATH, "src/test/resources/test_nifi_rules.json");
runner.setProperty(service,EasyRulesEngineService.RULES_FILE_TYPE, "JSON");
runner.setProperty(service,EasyRulesEngineService.RULES_FILE_FORMAT, "NIFI");
runner.setProperty(service,EasyRulesEngineService.IGNORE_CONDITION_ERRORS,"true");
runner.enableControllerService(service);
runner.assertValid(service);
Map<String, Object> facts = new HashMap<>();
facts.put("fakeMetric",60);
facts.put("predictedTimeToBytesBackpressureMillis",299999);
try {
List<Action> actions = service.fireRules(facts);
assertNotNull(actions);
assertEquals(actions.size(), 1);
}catch (PropertyAccessException pae){
fail();
}
}
@Test
public void testIgnoreConditionErrorsFalseMVEL() throws InitializationException, IOException {
final TestRunner runner = TestRunners.newTestRunner(TestProcessor.class);
final RulesEngineService service = new MockEasyRulesEngineService();
runner.addControllerService("easy-rules-engine-service-test",service);
runner.setProperty(service, EasyRulesEngineService.RULES_FILE_PATH, "src/test/resources/test_mvel_rules.json");
runner.setProperty(service,EasyRulesEngineService.RULES_FILE_TYPE, "JSON");
runner.setProperty(service,EasyRulesEngineService.RULES_FILE_FORMAT, "MVEL");
runner.setProperty(service,EasyRulesEngineService.IGNORE_CONDITION_ERRORS,"false");
runner.enableControllerService(service);
runner.assertValid(service);
Map<String, Object> facts = new HashMap<>();
facts.put("fakeMetric",60);
facts.put("predictedTimeToBytesBackpressureMillis",299999);
try {
service.fireRules(facts);
fail("Expected exception to be thrown");
}catch (PropertyAccessException pae){
assert true;
}
}
@Test
public void testIgnoreConditionErrorsTrueMVEL() throws InitializationException, IOException {
final TestRunner runner = TestRunners.newTestRunner(TestProcessor.class);
final RulesEngineService service = new MockEasyRulesEngineService();
runner.addControllerService("easy-rules-engine-service-test",service);
runner.setProperty(service, EasyRulesEngineService.RULES_FILE_PATH, "src/test/resources/test_mvel_rules.json");
runner.setProperty(service,EasyRulesEngineService.RULES_FILE_TYPE, "JSON");
runner.setProperty(service,EasyRulesEngineService.RULES_FILE_FORMAT, "MVEL");
runner.setProperty(service,EasyRulesEngineService.IGNORE_CONDITION_ERRORS,"true");
runner.enableControllerService(service);
runner.assertValid(service);
Map<String, Object> facts = new HashMap<>();
facts.put("fakeMetric",60);
facts.put("predictedTimeToBytesBackpressureMillis",299999);
try {
List<Action> actions = service.fireRules(facts);
assertNotNull(actions);
assertEquals(actions.size(), 1);
}catch (PropertyAccessException pae){
fail();
}
}
@Test
public void testIgnoreConditionErrorsFalseSPEL() throws InitializationException, IOException {
final TestRunner runner = TestRunners.newTestRunner(TestProcessor.class);
final RulesEngineService service = new MockEasyRulesEngineService();
runner.addControllerService("easy-rules-engine-service-test",service);
runner.setProperty(service, EasyRulesEngineService.RULES_FILE_PATH, "src/test/resources/test_bad_spel_rules.json");
runner.setProperty(service,EasyRulesEngineService.RULES_FILE_TYPE, "JSON");
runner.setProperty(service,EasyRulesEngineService.RULES_FILE_FORMAT, "SPEL");
runner.setProperty(service,EasyRulesEngineService.IGNORE_CONDITION_ERRORS,"false");
runner.enableControllerService(service);
runner.assertValid(service);
Map<String, Object> facts = new HashMap<>();
facts.put("fakeMetric",60);
facts.put("fakeMetric2",299999);
try {
service.fireRules(facts);
fail("Expected exception to be thrown");
}catch (Exception pae){
assert true;
}
}
@Test
public void testIgnoreConditionErrorsTrueSPEL() throws InitializationException, IOException {
final TestRunner runner = TestRunners.newTestRunner(TestProcessor.class);
final RulesEngineService service = new MockEasyRulesEngineService();
runner.addControllerService("easy-rules-engine-service-test",service);
runner.setProperty(service, EasyRulesEngineService.RULES_FILE_PATH, "src/test/resources/test_bad_spel_rules.json");
runner.setProperty(service,EasyRulesEngineService.RULES_FILE_TYPE, "JSON");
runner.setProperty(service,EasyRulesEngineService.RULES_FILE_FORMAT, "SPEL");
runner.setProperty(service,EasyRulesEngineService.IGNORE_CONDITION_ERRORS,"true");
runner.enableControllerService(service);
runner.assertValid(service);
Map<String, Object> facts = new HashMap<>();
facts.put("predictedQueuedCount",60);
facts.put("predictedTimeToBytesBackpressureMillis",299999);
try {
List<Action> actions = service.fireRules(facts);
assertNotNull(actions);
assertEquals(actions.size(), 1);
}catch (Exception pae){
fail();
}
}
private class MockEasyRulesEngineService extends EasyRulesEngineService {
}

View File

@ -0,0 +1,16 @@
[
{
"name": "Queue Size",
"description": "Queue size check greater than 50",
"priority": 1,
"condition": "#predictedQueuedCount > 50",
"actions": ["#predictedQueuedCount + 'is large'"]
},
{
"name": "Time To Back Pressure",
"description": "Back pressure time less than 5 minutes",
"priority": 2,
"condition": "predictedTimeToBytesBackpressureMillis < 300000 && #predictedTimeToBytesBackpressureMillis >= 0",
"actions": ["'System is approaching backpressure! Predicted time left: ' + #predictedTimeToBytesBackpressureMillis"]
}
]

View File

@ -10,7 +10,7 @@
"name": "Time To Back Pressure",
"description": "Back pressure time less than 5 minutes",
"priority": 2,
"condition": "predictedTimeToBytesBackpressureMillis >= 300000",
"condition": "predictedTimeToBytesBackpressureMillis < 300000 && predictedTimeToBytesBackpressureMillis >= 0",
"actions": ["System.out.println(\"Back Pressure prediction less than 5 minutes!\")"]
}
]

View File

@ -9,6 +9,6 @@ actions:
name: "Time To Back Pressure"
description: "Back pressure time less than 5 minutes"
priority: 2
condition: "predictedTimeToBytesBackpressureMillis >= 300000"
condition: "predictedTimeToBytesBackpressureMillis < 300000 && predictedTimeToBytesBackpressureMillis >= 0"
actions:
- "System.out.println(\"Back Pressure prediction less than 5 minutes!\")"

View File

@ -19,7 +19,7 @@
"name": "Time To Back Pressure",
"description": "Back pressure time less than 5 minutes",
"priority": 2,
"condition": "predictedTimeToBytesBackpressureMillis >= 300000",
"condition": "predictedTimeToBytesBackpressureMillis < 300000 && predictedTimeToBytesBackpressureMillis >= 0",
"actions": [
{
"type": "LOG",

View File

@ -14,7 +14,7 @@ facts:
name: "Time To Back Pressure"
description: "Back pressure time less than 5 minutes"
priority: 2
condition: "predictedTimeToBytesBackpressureMillis >= 300000"
condition: "predictedTimeToBytesBackpressureMillis < 300000 && predictedTimeToBytesBackpressureMillis >= 0"
actions:
- type: "LOG"
attributes:

View File

@ -10,7 +10,7 @@
"name": "Time To Back Pressure",
"description": "Back pressure time less than 5 minutes",
"priority": 2,
"condition": "#predictedTimeToBytesBackpressureMillis >= 300000",
"condition": "#predictedTimeToBytesBackpressureMillis < 300000 && #predictedTimeToBytesBackpressureMillis >= 0",
"actions": ["'System is approaching backpressure! Predicted time left: ' + #predictedTimeToBytesBackpressureMillis"]
}
]

View File

@ -9,6 +9,6 @@ actions:
name: "Time To Back Pressure"
description: "Back pressure time less than 5 minutes"
priority: 2
condition: "#predictedTimeToBytesBackpressureMillis >= 300000"
condition: "#predictedTimeToBytesBackpressureMillis < 300000 && #predictedTimeToBytesBackpressureMillis >= 0"
actions:
- "System.out.println(\"Back Pressure prediction less than 5 minutes!\")"