Skip to content

Commit

Permalink
[DROOLS-7589] ansible-rulebook : Throw Exception when heap reaches to… (
Browse files Browse the repository at this point in the history
#90)

* [DROOLS-7589] ansible-rulebook : Throw Exception when heap reaches to threshold

* - check memory every N events to avoid performance overhead

* More efficient counter to check memory occupation

* fix compilation

* wip

---------

Co-authored-by: Mario Fusco <[email protected]>
  • Loading branch information
tkobayas and mariofusco authored Nov 15, 2023
1 parent ab5e067 commit e3a6c73
Show file tree
Hide file tree
Showing 9 changed files with 236 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.drools.ansible.rulebook.integration.api;

import org.drools.ansible.rulebook.integration.api.rulesengine.MemoryMonitorUtil;
import org.drools.ansible.rulebook.integration.api.rulesengine.RulesEvaluator;
import org.drools.ansible.rulebook.integration.api.rulesengine.RulesExecutorSession;
import org.drools.ansible.rulebook.integration.api.rulesengine.SessionStats;
Expand Down Expand Up @@ -62,14 +63,17 @@ public long rulesCount() {
}

public CompletableFuture<Integer> executeFacts(String json) {
MemoryMonitorUtil.checkMemoryOccupation();
return rulesEvaluator.executeFacts(asFactMap(json));
}

public CompletableFuture<List<Match>> processFacts(String json) {
MemoryMonitorUtil.checkMemoryOccupation();
return rulesEvaluator.processFacts(asFactMap(json));
}

public CompletableFuture<List<Match>> processEvents(String json) {
MemoryMonitorUtil.checkMemoryOccupation();
return rulesEvaluator.processEvents(asFactMap(json));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package org.drools.ansible.rulebook.integration.api.rulesengine;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class MemoryMonitorUtil {

private static final Logger LOG = LoggerFactory.getLogger(MemoryMonitorUtil.class.getName());

public static final String MEMORY_OCCUPATION_PERCENTAGE_THRESHOLD_PROPERTY = "drools.memory.occupation.percentage.threshold";
private static final int DEFAULT_MEMORY_OCCUPATION_PERCENTAGE_THRESHOLD = 90;
private static final int MEMORY_OCCUPATION_PERCENTAGE_THRESHOLD;

// check memory per configured number of events are consumed
public static final String MEMORY_CHECK_EVENT_COUNT_THRESHOLD_PROPERTY = "drools.memory.check.event.count.threshold";
private static final int DEFAULT_MEMORY_CHECK_EVENT_COUNT_THRESHOLD = 64;
private static final int MEMORY_CHECK_EVENT_COUNT_MASK;
private static int COUNTER = 0;

static {
String memoryThresholdEnvValue = System.getenv("DROOLS_MEMORY_THRESHOLD");
if (memoryThresholdEnvValue != null && !memoryThresholdEnvValue.isEmpty()) {
// Environment variable takes precedence over system property
System.setProperty(MEMORY_OCCUPATION_PERCENTAGE_THRESHOLD_PROPERTY, memoryThresholdEnvValue);
}
MEMORY_OCCUPATION_PERCENTAGE_THRESHOLD = Integer.getInteger(MEMORY_OCCUPATION_PERCENTAGE_THRESHOLD_PROPERTY, DEFAULT_MEMORY_OCCUPATION_PERCENTAGE_THRESHOLD); // percentage
LOG.info("Memory occupation threshold set to {}%", MEMORY_OCCUPATION_PERCENTAGE_THRESHOLD);

String eventCountThresholdEnvValue = System.getenv("DROOLS_MEMORY_CHECK_EVENT_COUNT_THRESHOLD");
if (eventCountThresholdEnvValue != null && !eventCountThresholdEnvValue.isEmpty()) {
// Environment variable takes precedence over system property
System.setProperty(MEMORY_CHECK_EVENT_COUNT_THRESHOLD_PROPERTY, eventCountThresholdEnvValue);
}

int eventCountThreshold = Integer.getInteger(MEMORY_CHECK_EVENT_COUNT_THRESHOLD_PROPERTY, DEFAULT_MEMORY_CHECK_EVENT_COUNT_THRESHOLD); // number of events
MEMORY_CHECK_EVENT_COUNT_MASK = roundToPowerOfTwo(eventCountThreshold) - 1;
LOG.info("Memory check event count threshold set to {}", MEMORY_CHECK_EVENT_COUNT_MASK);
}

private MemoryMonitorUtil() {
// do not instantiate
}

public static void checkMemoryOccupation() {
if ((COUNTER++ & MEMORY_CHECK_EVENT_COUNT_MASK) == 0) {
// check memory occupation only once in 64 calls
return;
}
int memoryOccupationPercentage = getMemoryOccupationPercentage();
if (memoryOccupationPercentage > MEMORY_OCCUPATION_PERCENTAGE_THRESHOLD) {
// give GC a chance to free some memory
System.gc(); // NOSONAR
memoryOccupationPercentage = getMemoryOccupationPercentage();
if (memoryOccupationPercentage > MEMORY_OCCUPATION_PERCENTAGE_THRESHOLD) {
LOG.error("Memory occupation is above the threshold: {}% > {}%. MaxMemory = {}, UsedMemory = {}",
memoryOccupationPercentage, MEMORY_OCCUPATION_PERCENTAGE_THRESHOLD, Runtime.getRuntime().maxMemory(), Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory());
throw new MemoryThresholdReachedException(MEMORY_OCCUPATION_PERCENTAGE_THRESHOLD, memoryOccupationPercentage);
}
}
}

private static int getMemoryOccupationPercentage() {
return (int) ((100 * (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory())) / Runtime.getRuntime().maxMemory());
}

private static int roundToPowerOfTwo(final int value) {
if (value > Integer.MAX_VALUE) {
throw new IllegalArgumentException("There is no larger power of 2 int for value:" + value + " since it exceeds 2^31.");
}
if (value < 0) {
throw new IllegalArgumentException("Given value:" + value + ". Expecting value >= 0.");
}
return 1 << (32 - Integer.numberOfLeadingZeros(value - 1));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.drools.ansible.rulebook.integration.api.rulesengine;

public class MemoryThresholdReachedException extends RuntimeException {

private final int threshold;
private final int actual;

public MemoryThresholdReachedException(int threshold, int actual) {
this.threshold = threshold;
this.actual = actual;
}

@Override
public String getMessage() {
return "Memory threshold reached: " + actual + "% > " + threshold + "%";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ public boolean accept(Match match) {
boolean validMatch = isValidMatch(fhs);

if (validMatch) {
if (log.isInfoEnabled()) {
log.info(matchToString(match));
if (log.isDebugEnabled()) {
log.debug(matchToString(match));
}

Map<String, Object> metadata = match.getRule().getMetaData();
Expand Down
2 changes: 2 additions & 0 deletions drools-ansible-rulebook-integration-main/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@
<!-- for SlownessTest -->
<drools.delay.warning.threshold>2</drools.delay.warning.threshold>
</systemPropertyVariables>
<!-- <argLine>-Xmx500m -XX:+HeapDumpOnOutOfMemoryError</argLine>-->
<argLine>-Xmx500m</argLine>
</configuration>
</plugin>
</plugins>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
package org.drools.ansible.rulebook.integration.main;

import org.drools.ansible.rulebook.integration.api.io.JsonMapper;
import org.drools.ansible.rulebook.integration.core.jpy.AstRulesEngine;

import com.fasterxml.jackson.core.JacksonException;

import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.drools.ansible.rulebook.integration.api.io.JsonMapper;
import org.drools.ansible.rulebook.integration.core.jpy.AstRulesEngine;

public class Payload {

private final List<String> list;
Expand All @@ -22,6 +20,9 @@ public class Payload {

private int shutdown = 0;

// set true when matchedEvents occupies too much memory
private boolean discardMatchedEvents = false;

private Payload(List<String> list) {
this.list = list;
}
Expand Down Expand Up @@ -88,6 +89,11 @@ static Payload parsePayload(Map ruleSet) {
} catch (NullPointerException | NumberFormatException e) {
/* ignore */
}
try {
payload.discardMatchedEvents = Boolean.valueOf(sourcesArgs.get("discard_matched_events").toString());
} catch (NullPointerException | NumberFormatException e) {
/* ignore */
}

return payload;
}
Expand Down Expand Up @@ -128,7 +134,9 @@ public void run() {
for (int i = 0; i < payload.loopCount; i++) {
for (String p : payload.list) {
String resultJson = engine.assertEvent(sessionId, p);
returnedMatches.addAll(JsonMapper.readValueAsListOfMapOfStringAndObject(resultJson));
if (!payload.discardMatchedEvents) {
returnedMatches.addAll(JsonMapper.readValueAsListOfMapOfStringAndObject(resultJson));
}
sleepSeconds(payload.eventDelay);
}
sleepSeconds(payload.loopDelay);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package org.drools.ansible.rulebook.integration.main;

import org.drools.ansible.rulebook.integration.api.rulesengine.MemoryThresholdReachedException;
import org.drools.ansible.rulebook.integration.main.Main.ExecuteResult;
import org.junit.Ignore;
import org.junit.Test;

import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;

public class PerfTest {
Expand All @@ -12,6 +15,19 @@ public void testManyEvents() {
checkDuration("100k_event_rules_ast.json", 10_000);
}

@Ignore("Disabled by default, because it takes around 40 seconds")
@Test
public void testManyLargeEvents() {
// match_multiple_rules: false means events are removed after match. So this test will pass without throwing MemoryThresholdReachedException
checkDuration("1m_event_with_20kb_payload_rules_ast.json", 120_000);
}

@Test
public void testManyLargeEventsMatchMultipleRules() {
// match_multiple_rules: true means events are retained until TTL expires
assertThrows(MemoryThresholdReachedException.class, () -> checkDuration("1m_event_with_20kb_payload_match_multiple_rules_ast.json", 120_000));
}

@Test
public void testOnceAfter() {
checkDuration("56_once_after.json", 15_000);
Expand Down

Large diffs are not rendered by default.

Large diffs are not rendered by default.

0 comments on commit e3a6c73

Please sign in to comment.