Skip to content

Commit 28757e7

Browse files
authored
[DROOLS-7650] Returned match by accumulate_within contains a control fact (#154)
1 parent 267ba8e commit 28757e7

File tree

6 files changed

+212
-7
lines changed

6 files changed

+212
-7
lines changed

drools-ansible-rulebook-integration-api/src/main/java/org/drools/ansible/rulebook/integration/api/domain/temporal/AccumulateWithinDefinition.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,28 @@
11
package org.drools.ansible.rulebook.integration.api.domain.temporal;
22

33
import java.util.ArrayList;
4+
import java.util.Collection;
45
import java.util.HashMap;
56
import java.util.List;
67
import java.util.Map;
78

89
import org.drools.ansible.rulebook.integration.api.domain.RuleGenerationContext;
10+
import org.drools.ansible.rulebook.integration.api.rulesengine.EmptyMatchDecorator;
11+
import org.drools.ansible.rulebook.integration.api.rulesengine.RegisterOnlyAgendaFilter;
912
import org.drools.model.Drools;
1013
import org.drools.model.Rule;
14+
import org.drools.model.RuleItemBuilder;
1115
import org.drools.model.Variable;
1216
import org.drools.model.prototype.PrototypeDSL;
1317
import org.drools.model.view.CombinedExprViewItem;
1418
import org.drools.model.view.ViewItem;
1519
import org.kie.api.prototype.PrototypeEventInstance;
1620
import org.kie.api.prototype.PrototypeFactInstance;
21+
import org.kie.api.runtime.rule.Match;
1722
import org.slf4j.Logger;
1823
import org.slf4j.LoggerFactory;
1924

25+
import static org.drools.ansible.rulebook.integration.api.rulesengine.RegisterOnlyAgendaFilter.RULE_TYPE_TAG;
2026
import static org.drools.ansible.rulebook.integration.api.rulesengine.RegisterOnlyAgendaFilter.SYNTHETIC_RULE_TAG;
2127
import static org.drools.ansible.rulebook.integration.api.rulesmodel.PrototypeFactory.SYNTHETIC_PROTOTYPE_NAME;
2228
import static org.drools.ansible.rulebook.integration.api.rulesmodel.PrototypeFactory.getPrototypeEvent;
@@ -83,6 +89,24 @@ public class AccumulateWithinDefinition extends OnceAbstractTimeConstraint {
8389

8490
private final int threshold;
8591

92+
static {
93+
RegisterOnlyAgendaFilter.registerMatchTransformer(KEYWORD, AccumulateWithinDefinition::transformAccumulateWithinMatch);
94+
}
95+
96+
private static Match transformAccumulateWithinMatch(Match match) {
97+
EmptyMatchDecorator rewrittenMatch = new EmptyMatchDecorator(match);
98+
99+
// exclude control facts
100+
for (String declarationId : match.getDeclarationIds()) {
101+
Object value = match.getDeclarationValue(declarationId);
102+
if (value instanceof PrototypeFactInstance prototypeFactInstance
103+
&& !SYNTHETIC_PROTOTYPE_NAME.equals(prototypeFactInstance.getPrototype().getName())) {
104+
rewrittenMatch.withBoundObject(declarationId, value);
105+
}
106+
}
107+
return rewrittenMatch;
108+
}
109+
86110
public AccumulateWithinDefinition(TimeAmount timeAmount, int threshold, List<GroupByAttribute> groupByAttributes) {
87111
super(timeAmount, groupByAttributes);
88112
this.threshold = threshold;
@@ -112,6 +136,13 @@ public ViewItem processTimeConstraint(String ruleName, ViewItem pattern) {
112136
new ViewItem[]{guardedPattern, thresholdMetPattern});
113137
}
114138

139+
@Override
140+
public Rule buildTimedRule(String ruleName, RuleItemBuilder pattern, RuleItemBuilder consequence) {
141+
return rule(ruleName)
142+
.metadata(RULE_TYPE_TAG, KEYWORD)
143+
.build(pattern, consequence);
144+
}
145+
115146
@Override
116147
public Variable<?>[] getTimeConstraintConsequenceVariables() {
117148
return new Variable[]{getPatternVariable(), getControlVariable()};

drools-ansible-rulebook-integration-api/src/test/java/org/drools/ansible/rulebook/integration/api/AccumulateWithinTest.java

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import org.kie.api.prototype.PrototypeFactInstance;
1212
import org.kie.api.runtime.rule.Match;
1313

14+
import static org.assertj.core.api.Assertions.assertThat;
1415
import static org.junit.jupiter.api.Assertions.assertEquals;
1516

1617
public class AccumulateWithinTest {
@@ -73,6 +74,9 @@ void testAccumulateWithinThresholdMet() {
7374
matchedRules = rulesExecutor.processEvents("{ \"sensu\": { \"process\": { \"type\":\"alert\" }, \"host\":\"h1\" }, \"sequence\": 3 }").join();
7475
assertEquals(1, matchedRules.size());
7576

77+
// Verify that there is no unexpected event in the returned match
78+
assertThat(matchedRules.get(0).getDeclarationIds()).containsExactly("m");
79+
7680
// Verify metadata
7781
PrototypeFactInstance fact = (PrototypeFactInstance) matchedRules.get(0).getDeclarationValue("m");
7882
Map map = (Map) fact.asMap();
@@ -97,6 +101,76 @@ void testAccumulateWithinThresholdMet() {
97101
rulesExecutor.dispose();
98102
}
99103

104+
@Test
105+
void testAccumulateWithinThresholdMet_assignment() {
106+
String json =
107+
"""
108+
{
109+
"rules":[
110+
{
111+
"Rule":{
112+
"condition":{
113+
"AllCondition":[
114+
{
115+
"AssignmentExpression":{
116+
"lhs":{
117+
"Events":"singleton"
118+
},
119+
"rhs":{
120+
"EqualsExpression":{
121+
"lhs":{
122+
"Event":"sensu.process.type"
123+
},
124+
"rhs":{
125+
"String":"alert"
126+
}
127+
}
128+
}
129+
}
130+
}
131+
]
132+
},
133+
"action":{
134+
"assert_fact":{
135+
"ruleset":"Test rules4",
136+
"fact":{
137+
"j":1
138+
}
139+
}
140+
},
141+
"throttle": {
142+
"group_by_attributes": [
143+
"event.sensu.host",
144+
"event.sensu.process.type"
145+
],
146+
"accumulate_within": "10 minutes",
147+
"threshold": 3
148+
}
149+
}
150+
}
151+
]
152+
}
153+
""";
154+
// This test focuses on that the returned match contains the expected declaration id
155+
RulesExecutor rulesExecutor = RulesExecutorFactory.createFromJson(RuleNotation.CoreNotation.INSTANCE.withOptions(RuleConfigurationOption.USE_PSEUDO_CLOCK), json);
156+
rulesExecutor.processEvents("{ \"sensu\": { \"process\": { \"type\":\"alert\" }, \"host\":\"h1\" }, \"sequence\": 1 }").join();
157+
rulesExecutor.processEvents("{ \"sensu\": { \"process\": { \"type\":\"alert\" }, \"host\":\"h1\" }, \"sequence\": 2 }").join();
158+
List<Match> matchedRules = rulesExecutor.processEvents("{ \"sensu\": { \"process\": { \"type\":\"alert\" }, \"host\":\"h1\" }, \"sequence\": 3 }").join();
159+
160+
// Verify that there is no unexpected event in the returned match
161+
assertThat(matchedRules.get(0).getDeclarationIds()).containsExactly("singleton");
162+
163+
// Verify metadata
164+
PrototypeFactInstance fact = (PrototypeFactInstance) matchedRules.get(0).getDeclarationValue("singleton");
165+
Map map = fact.asMap();
166+
Map ruleEngineMeta = (Map) ((Map) map.get(RulesModelUtil.META_FIELD)).get(RulesModelUtil.RULE_ENGINE_META_FIELD);
167+
assertEquals(new TimeAmount(10, TimeUnit.MINUTES).toString(), ruleEngineMeta.get("accumulate_within_time_window"));
168+
assertEquals(3, ruleEngineMeta.get("threshold"));
169+
assertEquals(3, fact.asMap().get("sequence"));
170+
171+
rulesExecutor.dispose();
172+
}
173+
100174
@Test
101175
void testAccumulateWithinTimeout() {
102176
String json =

drools-ansible-rulebook-integration-api/src/test/java/org/drools/ansible/rulebook/integration/api/OnceAfterTest.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,9 @@ void testOnceAfterWithOr() {
107107
assertEquals(3, stats.getEventsSuppressed());
108108
assertEquals(0, stats.getPermanentStorageCount());
109109

110+
// Verify that there is no unexpected event in the returned match
111+
assertThat(matchedRules.get(0).getDeclarationIds()).containsExactlyInAnyOrder("m_0", "m_1", "m_2");
112+
110113
for (int i = 0; i < 3; i++) {
111114
PrototypeFactInstance fact = (PrototypeFactInstance) matchedRules.get(0).getDeclarationValue("m_" + i);
112115
String host = evalAgainstFact(fact, "meta.hosts").toString();

drools-ansible-rulebook-integration-api/src/test/java/org/drools/ansible/rulebook/integration/api/OnceWithinTest.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import java.util.List;
1111

12+
import static org.assertj.core.api.Assertions.assertThat;
1213
import static org.junit.jupiter.api.Assertions.assertEquals;
1314
import java.util.Map;
1415
import java.util.concurrent.TimeUnit;
@@ -126,6 +127,9 @@ private void onceWithinTest(String json) {
126127
List<Match> matchedRules = rulesExecutor.processEvents("{ \"sensu\": { \"process\": { \"type\":\"alert\" }, \"host\":\"h1\" } }").join();
127128
assertEquals(1, matchedRules.size());
128129

130+
// Verify that there is no unexpected event in the returned match
131+
assertThat(matchedRules.get(0).getDeclarationIds()).containsExactly("singleton");
132+
129133
PrototypeFactInstance fact = (PrototypeFactInstance) matchedRules.get(0).getDeclarationValue("singleton");
130134
Map map = (Map) fact.asMap();
131135
Map ruleEngineMeta = (Map) ((Map)map.get(RulesModelUtil.META_FIELD)).get(RulesModelUtil.RULE_ENGINE_META_FIELD);

drools-ansible-rulebook-integration-api/src/test/java/org/drools/ansible/rulebook/integration/api/TimeWindowTest.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import org.junit.jupiter.api.Test;
77
import org.kie.api.runtime.rule.Match;
88

9+
import static org.assertj.core.api.Assertions.assertThat;
910
import static org.junit.jupiter.api.Assertions.assertEquals;
1011

1112
public class TimeWindowTest {
@@ -134,6 +135,9 @@ private void timeWindowTest(String json) {
134135
matchedRules = rulesExecutor.processEvents( "{ \"sensu\": { \"process\": { \"status\":\"stopped\" } } }" ).join();
135136
assertEquals( 1, matchedRules.size() );
136137

138+
// Verify that there is no unexpected event in the returned match
139+
assertThat(matchedRules.get(0).getDeclarationIds()).containsExactlyInAnyOrder("m_0", "m_1", "m_2");
140+
137141
rulesExecutor.dispose();
138142
}
139143
}

drools-ansible-rulebook-integration-tests/src/test/java/org/drools/ansible/rulebook/integration/test/jpy/AstRulesEngineTest.java

Lines changed: 96 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,5 @@
11
package org.drools.ansible.rulebook.integration.test.jpy;
22

3-
import org.drools.ansible.rulebook.integration.api.JsonTest;
4-
import org.drools.ansible.rulebook.integration.api.io.JsonMapper;
5-
import org.drools.ansible.rulebook.integration.core.jpy.AstRulesEngine;
6-
import org.junit.jupiter.api.Test;
7-
import org.junit.jupiter.api.Timeout;
8-
93
import java.io.DataInputStream;
104
import java.io.IOException;
115
import java.io.InputStream;
@@ -14,7 +8,19 @@
148
import java.util.List;
159
import java.util.Map;
1610

17-
import static org.junit.jupiter.api.Assertions.*;
11+
import org.drools.ansible.rulebook.integration.api.JsonTest;
12+
import org.drools.ansible.rulebook.integration.api.io.JsonMapper;
13+
import org.drools.ansible.rulebook.integration.core.jpy.AstRulesEngine;
14+
import org.junit.jupiter.api.Test;
15+
import org.junit.jupiter.api.Timeout;
16+
17+
import static org.assertj.core.api.Assertions.assertThat;
18+
import static org.junit.jupiter.api.Assertions.assertEquals;
19+
import static org.junit.jupiter.api.Assertions.assertFalse;
20+
import static org.junit.jupiter.api.Assertions.assertNotNull;
21+
import static org.junit.jupiter.api.Assertions.assertThrows;
22+
import static org.junit.jupiter.api.Assertions.assertTrue;
23+
import static org.junit.jupiter.api.Assertions.fail;
1824

1925

2026
public class AstRulesEngineTest {
@@ -334,4 +340,87 @@ void testAssertEvent() throws IOException {
334340
assertEquals(((Map) ((Map) v.get(0).get("Create Snapshot")).get("m")).get("type"), "MODIFIED");
335341
}
336342
}
343+
344+
@Test
345+
void testAccumulateWithinThresholdMet() {
346+
String rules = """
347+
{
348+
"rules":[
349+
{
350+
"Rule":{
351+
"name":"Test accumulate_within",
352+
"condition":{
353+
"AllCondition":[
354+
{
355+
"EqualsExpression":{
356+
"lhs":{
357+
"Event":"sensu.process.type"
358+
},
359+
"rhs":{
360+
"String":"alert"
361+
}
362+
}
363+
}
364+
]
365+
},
366+
"action":{
367+
"assert_fact":{
368+
"ruleset":"Test rules4",
369+
"fact":{
370+
"j":1
371+
}
372+
}
373+
},
374+
"throttle": {
375+
"group_by_attributes": [
376+
"event.sensu.host",
377+
"event.sensu.process.type"
378+
],
379+
"accumulate_within": "10 minutes",
380+
"threshold": 3
381+
}
382+
}
383+
}
384+
]
385+
}
386+
""";
387+
388+
try (AstRulesEngine engine = new AstRulesEngine()) {
389+
long sessionId = engine.createRuleset(rules);
390+
391+
// First event with sequence=1 - no fire
392+
String result1 = engine.assertEvent(sessionId, "{ \"sensu\": { \"process\": { \"type\":\"alert\" }, \"host\":\"h1\" }, \"sequence\": 1 }");
393+
assertEquals("[]", result1);
394+
395+
// Second event with sequence=2 - no fire
396+
String result2 = engine.assertEvent(sessionId, "{ \"sensu\": { \"process\": { \"type\":\"alert\" }, \"host\":\"h1\" }, \"sequence\": 2 }");
397+
assertEquals("[]", result2);
398+
399+
// Third event with sequence=3 - threshold met, should fire
400+
String result3 = engine.assertEvent(sessionId, "{ \"sensu\": { \"process\": { \"type\":\"alert\" }, \"host\":\"h1\" }, \"sequence\": 3 }");
401+
List<Map<String, Object>> matchedRules = JsonMapper.readValueAsListOfMapOfStringAndObject(result3);
402+
assertEquals(1, matchedRules.size());
403+
404+
// Verify that the returned event is the last one (sequence=3)
405+
Map<String, Object> ruleMatch = matchedRules.get(0);
406+
Map<String, Object> innerMatch = (Map<String, Object>) ruleMatch.get("Test accumulate_within");
407+
assertThat(innerMatch).containsOnlyKeys("m"); // returned match should not contain control events
408+
Map<String, Object> eventData = (Map<String, Object>) innerMatch.get("m");
409+
assertEquals(3, eventData.get("sequence"));
410+
411+
// Verify session stats
412+
String sessionStats = engine.sessionStats(sessionId);
413+
Map<String, Object> statsMap = JsonMapper.readValueAsMapOfStringAndObject(sessionStats);
414+
assertEquals(1, statsMap.get("eventsMatched"));
415+
assertEquals(3, statsMap.get("eventsProcessed"));
416+
assertEquals(2, statsMap.get("eventsSuppressed"));
417+
assertEquals(0, statsMap.get("permanentStorageCount"));
418+
419+
// Fourth event - starts new accumulation window, no fire
420+
String result4 = engine.assertFact(sessionId, "{ \"sensu\": { \"process\": { \"type\":\"alert\" }, \"host\":\"h1\" }, \"sequence\": 4 }");
421+
assertEquals("[]", result4);
422+
423+
engine.dispose(sessionId);
424+
}
425+
}
337426
}

0 commit comments

Comments
 (0)