You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@unomi.apache.org by sh...@apache.org on 2021/07/30 06:45:32 UTC
[unomi] branch unomi-1.6.x updated: UNOMI-188 Rule event type
optimization (#321)
This is an automated email from the ASF dual-hosted git repository.
shuber pushed a commit to branch unomi-1.6.x
in repository https://gitbox.apache.org/repos/asf/unomi.git
The following commit(s) were added to refs/heads/unomi-1.6.x by this push:
new 196693c UNOMI-188 Rule event type optimization (#321)
196693c is described below
commit 196693c2a0c4c888eec22eabb1c2d512bbc27ae1
Author: Serge Huber <sh...@jahia.com>
AuthorDate: Thu Jul 29 17:00:07 2021 +0200
UNOMI-188 Rule event type optimization (#321)
* UNOMI-188 Rule event type optimization
- New optimization for rules : rule condition are parsed to determine the event types they handle. This is done using a new ParserHelper method that navigates the tree of conditions to resolve which eventTypeCondition are used.
- Settings to deactivate the new optimization in case it causes issues
- Integration tests to validate that the parsing of conditions is behaving as expected
- Performance tests to validate the performance improvement of the optimization
* UNOMI-188 Rule event type optimization
Improvements on first draft:
- Remove getSetting/setSetting on RuleService and replaced it with OSGi configuration admin. This required making sure that services are properly reinjected (manually) into the integration test classes
- Added check in integration test to make sure that the complex rule DOES get matched for non-view events (fixing a bug in the first draft)
- Fix problem with negated rules not being used when no rule matched specific event type
- Removed updates of rulesByEventType structure in get/setRule calls
- Refactored visit pattern to add a postVisit method and remove the stack passed on the parameters
* UNOMI-188 Rule event type optimization
- Fixed some issues with NullPointerException in the RestServer when it was restarting
- Moved the update of the rules by event type into a separate method
- Added the missing call to the postVisit method in visitConditions.
(cherry picked from commit 79349d3968f830edb9e5022e7bb69e0b3cc48669)
---
.../apache/unomi/api/services/RulesService.java | 14 +++
.../test/java/org/apache/unomi/itests/AllITs.java | 41 +++----
.../test/java/org/apache/unomi/itests/BaseIT.java | 77 ++++++++++++-
.../org/apache/unomi/itests/ConditionBuilder.java | 6 +-
.../org/apache/unomi/itests/RuleServiceIT.java | 127 +++++++++++++++++++--
.../main/resources/etc/custom.system.properties | 4 +
.../org/apache/unomi/rest/server/RestServer.java | 14 ++-
.../apache/unomi/services/impl/ParserHelper.java | 53 +++++++++
.../services/impl/rules/RulesServiceImpl.java | 68 +++++++++--
.../resources/OSGI-INF/blueprint/blueprint.xml | 2 +
.../main/resources/org.apache.unomi.services.cfg | 5 +
11 files changed, 363 insertions(+), 48 deletions(-)
diff --git a/api/src/main/java/org/apache/unomi/api/services/RulesService.java b/api/src/main/java/org/apache/unomi/api/services/RulesService.java
index 786ecfd..817e088 100644
--- a/api/src/main/java/org/apache/unomi/api/services/RulesService.java
+++ b/api/src/main/java/org/apache/unomi/api/services/RulesService.java
@@ -17,6 +17,7 @@
package org.apache.unomi.api.services;
+import org.apache.unomi.api.Event;
import org.apache.unomi.api.Item;
import org.apache.unomi.api.Metadata;
import org.apache.unomi.api.PartialList;
@@ -104,4 +105,17 @@ public interface RulesService {
* @return the Set of tracked conditions for the specified item
*/
Set<Condition> getTrackedConditions(Item item);
+
+ /**
+ * Retrieves all the matching rules for a specific event
+ * @param event the event we want to retrieve all the matching rules for
+ * @return a set of rules that match the event passed in the parameters
+ */
+ public Set<Rule> getMatchingRules(Event event);
+
+ /**
+ * Refresh the rules for this instance by reloading them from the persistence backend
+ */
+ public void refreshRules();
+
}
diff --git a/itests/src/test/java/org/apache/unomi/itests/AllITs.java b/itests/src/test/java/org/apache/unomi/itests/AllITs.java
index 62bb231..0b14624 100644
--- a/itests/src/test/java/org/apache/unomi/itests/AllITs.java
+++ b/itests/src/test/java/org/apache/unomi/itests/AllITs.java
@@ -28,26 +28,27 @@ import org.junit.runners.Suite.SuiteClasses;
*/
@RunWith(Suite.class)
@SuiteClasses({
- BasicIT.class,
- ConditionEvaluatorIT.class,
- ConditionESQueryBuilderIT.class,
- SegmentIT.class,
- ProfileServiceIT.class,
- ProfileImportBasicIT.class,
- ProfileImportSurfersIT.class,
- ProfileImportRankingIT.class,
- ProfileImportActorsIT.class,
- ProfileExportIT.class,
- ProfileMergeIT.class,
- EventServiceIT.class,
- PropertiesUpdateActionIT.class,
- IncrementPropertyIT.class,
- InputValidationIT.class,
- CopyPropertiesActionIT.class,
- ModifyConsentIT.class,
- PatchIT.class,
- ContextServletIT.class,
- SecurityIT.class
+ BasicIT.class,
+ ConditionEvaluatorIT.class,
+ ConditionESQueryBuilderIT.class,
+ SegmentIT.class,
+ ProfileServiceIT.class,
+ ProfileImportBasicIT.class,
+ ProfileImportSurfersIT.class,
+ ProfileImportRankingIT.class,
+ ProfileImportActorsIT.class,
+ ProfileExportIT.class,
+ ProfileMergeIT.class,
+ EventServiceIT.class,
+ PropertiesUpdateActionIT.class,
+ CopyPropertiesActionIT.class,
+ IncrementPropertyIT.class,
+ InputValidationIT.class,
+ ModifyConsentIT.class,
+ PatchIT.class,
+ ContextServletIT.class,
+ SecurityIT.class,
+ RuleServiceIT.class,
})
public class AllITs {
}
diff --git a/itests/src/test/java/org/apache/unomi/itests/BaseIT.java b/itests/src/test/java/org/apache/unomi/itests/BaseIT.java
index 42e1318..0519adf 100644
--- a/itests/src/test/java/org/apache/unomi/itests/BaseIT.java
+++ b/itests/src/test/java/org/apache/unomi/itests/BaseIT.java
@@ -40,16 +40,18 @@ import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy;
import org.ops4j.pax.exam.spi.reactors.PerSuite;
import org.ops4j.pax.exam.util.Filter;
import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceEvent;
+import org.osgi.framework.ServiceListener;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.cm.ConfigurationAdmin;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import java.io.File;
import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
+import java.util.concurrent.CountDownLatch;
import java.util.function.Predicate;
import java.util.function.Supplier;
@@ -87,6 +89,10 @@ public abstract class BaseIT {
@Inject @Filter(timeout = 600000)
protected BundleWatcher bundleWatcher;
+ @Inject
+ @Filter(timeout = 600000)
+ protected ConfigurationAdmin configurationAdmin;
+
@Before
public void waitForStartup() throws InterruptedException {
while (!bundleWatcher.isStartupComplete()) {
@@ -258,4 +264,67 @@ public abstract class BaseIT {
return objectMapper.writeValueAsString(objectMapper.readTree(jsonString));
}
+ public void updateServices() throws InterruptedException {
+ persistenceService = getService(PersistenceService.class);
+ definitionsService = getService(DefinitionsService.class);
+ }
+
+ public void updateConfiguration(String serviceName, String configPid, String propName, Object propValue) throws InterruptedException, IOException {
+ org.osgi.service.cm.Configuration cfg = configurationAdmin.getConfiguration(configPid);
+ Dictionary<String, Object> props = cfg.getProperties();
+ props.put(propName, propValue);
+
+ waitForReRegistration(serviceName, () -> {
+ try {
+ cfg.update(props);
+ } catch (IOException ignored) {
+ }
+ });
+
+ waitForStartup();
+
+ // we must update our service objects now
+ updateServices();
+ }
+
+ public void waitForReRegistration(String serviceName, Runnable trigger) throws InterruptedException {
+ CountDownLatch latch1 = new CountDownLatch(2);
+ ServiceListener serviceListener = e -> {
+ LOGGER.info("Service {} {}", e.getServiceReference().getProperty("objectClass"), serviceEventTypeToString(e));
+ if ((e.getType() == ServiceEvent.UNREGISTERING || e.getType() == ServiceEvent.REGISTERED)
+ && ((String[])e.getServiceReference().getProperty("objectClass"))[0].equals(serviceName)) {
+ latch1.countDown();
+ }
+ };
+ bundleContext.addServiceListener(serviceListener);
+ trigger.run();
+ latch1.await();
+ bundleContext.removeServiceListener(serviceListener);
+ }
+
+ public String serviceEventTypeToString(ServiceEvent serviceEvent) {
+ switch (serviceEvent.getType()) {
+ case ServiceEvent.MODIFIED:
+ return "modified";
+ case ServiceEvent.REGISTERED:
+ return "registered";
+ case ServiceEvent.UNREGISTERING:
+ return "unregistering";
+ case ServiceEvent.MODIFIED_ENDMATCH:
+ return "modified endmatch";
+ default:
+ return "unknown type " + serviceEvent.getType();
+ }
+ }
+
+ public <T> T getService(Class<T> serviceClass) throws InterruptedException {
+ ServiceReference<T> serviceReference = bundleContext.getServiceReference(serviceClass);
+ while (serviceReference == null) {
+ LOGGER.info("Waiting for service {} to become available", serviceClass.getName());
+ Thread.sleep(1000);
+ serviceReference = bundleContext.getServiceReference(serviceClass);
+ }
+ return bundleContext.getService(serviceReference);
+ }
+
}
diff --git a/itests/src/test/java/org/apache/unomi/itests/ConditionBuilder.java b/itests/src/test/java/org/apache/unomi/itests/ConditionBuilder.java
index 2420aed..2d01f97 100644
--- a/itests/src/test/java/org/apache/unomi/itests/ConditionBuilder.java
+++ b/itests/src/test/java/org/apache/unomi/itests/ConditionBuilder.java
@@ -63,6 +63,10 @@ public class ConditionBuilder {
return new PropertyCondition(conditionTypeId, propertyName, definitionsService);
}
+ public ConditionItem condition(String conditionTypeId) {
+ return new ConditionItem(conditionTypeId, definitionsService);
+ }
+
public abstract class ComparisonCondition extends ConditionItem {
ComparisonCondition(String conditionTypeId, DefinitionsService definitionsService) {
@@ -287,7 +291,7 @@ public class ConditionBuilder {
}
}
- public abstract class ConditionItem {
+ public class ConditionItem {
protected Condition condition;
diff --git a/itests/src/test/java/org/apache/unomi/itests/RuleServiceIT.java b/itests/src/test/java/org/apache/unomi/itests/RuleServiceIT.java
index ee25a09..7a4488b 100644
--- a/itests/src/test/java/org/apache/unomi/itests/RuleServiceIT.java
+++ b/itests/src/test/java/org/apache/unomi/itests/RuleServiceIT.java
@@ -16,25 +16,32 @@
*/
package org.apache.unomi.itests;
-import org.apache.unomi.api.Metadata;
+import org.apache.unomi.api.*;
import org.apache.unomi.api.rules.Rule;
-import org.apache.unomi.api.services.DefinitionsService;
+import org.apache.unomi.api.services.EventService;
import org.apache.unomi.api.services.RulesService;
-import org.apache.unomi.persistence.spi.PersistenceService;
import org.junit.Before;
import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.junit.PaxExam;
+import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy;
+import org.ops4j.pax.exam.spi.reactors.PerSuite;
import org.ops4j.pax.exam.util.Filter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNull;
+import java.io.IOException;
+import java.util.*;
+
+import static org.junit.Assert.*;
/**
* Integration tests for the Unomi rule service.
*/
+@RunWith(PaxExam.class)
+@ExamReactorStrategy(PerSuite.class)
public class RuleServiceIT extends BaseIT {
private final static Logger LOGGER = LoggerFactory.getLogger(RuleServiceIT.class);
@@ -48,11 +55,7 @@ public class RuleServiceIT extends BaseIT {
@Inject
@Filter(timeout = 600000)
- protected PersistenceService persistenceService;
-
- @Inject
- @Filter(timeout = 600000)
- protected DefinitionsService definitionsService;
+ protected EventService eventService;
@Before
public void setUp() {
@@ -74,5 +77,109 @@ public class RuleServiceIT extends BaseIT {
assertNull("Expected rule actions to be null", nullRule.getActions());
assertNull("Expected rule condition to be null", nullRule.getCondition());
assertEquals("Invalid rule name", TEST_RULE_ID + "_name", nullRule.getMetadata().getName());
+ rulesService.removeRule(TEST_RULE_ID);
+ refreshPersistence();
+ rulesService.refreshRules();
+ }
+
+ @Test
+ public void testRuleEventTypeOptimization() throws InterruptedException {
+
+ ConditionBuilder builder = new ConditionBuilder(definitionsService);
+ Rule simpleEventTypeRule = new Rule(new Metadata(TEST_SCOPE, "simple-event-type-rule", "Simple event type rule", "A rule with a simple condition to match an event type"));
+ simpleEventTypeRule.setCondition(builder.condition("eventTypeCondition").parameter("eventTypeId", "view").build());
+ rulesService.setRule(simpleEventTypeRule);
+ Rule complexEventTypeRule = new Rule(new Metadata(TEST_SCOPE, "complex-event-type-rule", "Complex event type rule", "A rule with a complex condition to match multiple event types with negations"));
+ complexEventTypeRule.setCondition(
+ builder.not(
+ builder.or(
+ builder.condition("eventTypeCondition").parameter( "eventTypeId", "view"),
+ builder.condition("eventTypeCondition").parameter("eventTypeId", "form")
+ )
+ ).build()
+ );
+ rulesService.setRule(complexEventTypeRule);
+
+ refreshPersistence();
+ rulesService.refreshRules();
+
+ Profile profile = new Profile(UUID.randomUUID().toString());
+ Session session = new Session(UUID.randomUUID().toString(), profile, new Date(), TEST_SCOPE);
+ Event viewEvent = generateViewEvent(session, profile);
+ Set<Rule> matchingRules = rulesService.getMatchingRules(viewEvent);
+
+ assertTrue("Simple rule should be matched", matchingRules.contains(simpleEventTypeRule));
+ assertFalse("Complex rule should NOT be matched", matchingRules.contains(complexEventTypeRule));
+
+ Event loginEvent = new Event(UUID.randomUUID().toString(), "login", session, profile, TEST_SCOPE, null, null, new Date());
+ matchingRules = rulesService.getMatchingRules(loginEvent);
+ assertTrue("Complex rule should be matched", matchingRules.contains(complexEventTypeRule));
+ assertFalse("Simple rule should NOT be matched", matchingRules.contains(simpleEventTypeRule));
+
+ rulesService.removeRule(simpleEventTypeRule.getItemId());
+ rulesService.removeRule(complexEventTypeRule.getItemId());
+ refreshPersistence();
+ rulesService.refreshRules();
+ }
+
+ @Test
+ public void testRuleOptimizationPerf() throws NoSuchFieldException, IllegalAccessException, IOException, InterruptedException {
+ Profile profile = new Profile(UUID.randomUUID().toString());
+ Session session = new Session(UUID.randomUUID().toString(), profile, new Date(), TEST_SCOPE);
+
+ updateConfiguration(RulesService.class.getName(), "org.apache.unomi.services", "rules.optimizationActivated", "false");
+
+ LOGGER.info("Running unoptimized rules performance test...");
+ long unoptimizedRunTime = runEventTest(profile, session);
+
+ updateConfiguration(RulesService.class.getName(), "org.apache.unomi.services", "rules.optimizationActivated", "true");
+
+ LOGGER.info("Running optimized rules performance test...");
+ long optimizedRunTime = runEventTest(profile, session);
+
+ LOGGER.info("Unoptimized run time = {}ms, optimized run time = {}ms. Improvement={}x", unoptimizedRunTime, optimizedRunTime, ((double) unoptimizedRunTime) / ((double) optimizedRunTime));
+ assertTrue("Optimized run time should be smaller than unoptimized", unoptimizedRunTime > optimizedRunTime);
+ }
+
+ private long runEventTest(Profile profile, Session session) {
+ LOGGER.info("eventService={}", eventService);
+ Event viewEvent = generateViewEvent(session, profile);
+ int loopCount = 0;
+ long startTime = System.currentTimeMillis();
+ while (loopCount < 500) {
+ eventService.send(viewEvent);
+ viewEvent = generateViewEvent(session, profile);
+ loopCount++;
+ }
+ return System.currentTimeMillis() - startTime;
+ }
+
+ private Event generateViewEvent(Session session, Profile profile) {
+ CustomItem sourceItem = new CustomItem();
+ sourceItem.setScope(TEST_SCOPE);
+
+ CustomItem targetItem = new CustomItem();
+ targetItem.setScope(TEST_SCOPE);
+ Map<String,Object> targetProperties = new HashMap<>();
+
+ Map<String,Object> pageInfo = new HashMap<>();
+ pageInfo.put("language", "en");
+ pageInfo.put("destinationURL", "https://www.acme.com/test-page.html");
+ pageInfo.put("referringURL", "https://unomi.apache.org");
+ pageInfo.put("pageID", "ITEM_ID_PAGE");
+ pageInfo.put("pagePath", "/test-page.html");
+ pageInfo.put("pageName", "Test page");
+
+ targetProperties.put("pageInfo", pageInfo);
+
+ targetItem.setProperties(targetProperties);
+ return new Event(UUID.randomUUID().toString(), "view", session, profile, TEST_SCOPE, sourceItem, targetItem, new Date());
+ }
+
+ @Override
+ public void updateServices() throws InterruptedException {
+ super.updateServices();
+ rulesService = getService(RulesService.class);
+ eventService = getService(EventService.class);
}
}
diff --git a/package/src/main/resources/etc/custom.system.properties b/package/src/main/resources/etc/custom.system.properties
index 051ce2f..c2be62e 100644
--- a/package/src/main/resources/etc/custom.system.properties
+++ b/package/src/main/resources/etc/custom.system.properties
@@ -177,6 +177,10 @@ org.apache.unomi.segment.recalculate.period=${env:UNOMI_SEGMENT_RECALCULATE_PERI
org.apache.unomi.rules.refresh.interval=${env:UNOMI_RULES_REFRESH_INTERVAL:-1000}
# The interval in milliseconds to use to reload the rules statistics
org.apache.unomi.rules.statistics.refresh.interval=${env:UNOMI_RULES_STATISTICS_REFRESH_INTERVAL:-10000}
+# If this setting is active, the rules engine will try to classify the events by event type internally which makes
+# rules execution a lot faster. If there are any problems detected with rules execution, you might want to try to turn
+# off the optimization and file a bug report if this fixed the problem.
+org.apache.unomi.rules.optimizationActivated=${env:UNOMI_RULES_OPTIMIZATION_ACTIVATED:-true}
#######################################################################################################################
## Third Party server settings ##
diff --git a/rest/src/main/java/org/apache/unomi/rest/server/RestServer.java b/rest/src/main/java/org/apache/unomi/rest/server/RestServer.java
index d910012..c260ffe 100644
--- a/rest/src/main/java/org/apache/unomi/rest/server/RestServer.java
+++ b/rest/src/main/java/org/apache/unomi/rest/server/RestServer.java
@@ -125,6 +125,15 @@ public class RestServer {
@Override
public Object addingService(ServiceReference reference) {
Object serviceBean = bundleContext.getService(reference);
+ while (serviceBean == null) {
+ logger.info("Waiting for service " + reference.getProperty("objectClass") + " to become available...");
+ serviceBean = bundleContext.getService(reference);
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException e) {
+ logger.warn("Interrupted thread exception", e);
+ }
+ }
logger.info("Registering JAX RS service " + serviceBean.getClass().getName());
serviceBeans.add(serviceBean);
timeOfLastUpdate = System.currentTimeMillis();
@@ -141,9 +150,8 @@ public class RestServer {
@Override
public void removedService(ServiceReference reference, Object service) {
- Object serviceBean = bundleContext.getService(reference);
- logger.info("Removing JAX RS service " + serviceBean.getClass().getName());
- serviceBeans.remove(serviceBean);
+ logger.info("Removing JAX RS service " + service.getClass().getName());
+ serviceBeans.remove(service);
timeOfLastUpdate = System.currentTimeMillis();
refreshServer();
}
diff --git a/services/src/main/java/org/apache/unomi/services/impl/ParserHelper.java b/services/src/main/java/org/apache/unomi/services/impl/ParserHelper.java
index ef9b9de..0b90ec5 100644
--- a/services/src/main/java/org/apache/unomi/services/impl/ParserHelper.java
+++ b/services/src/main/java/org/apache/unomi/services/impl/ParserHelper.java
@@ -63,6 +63,10 @@ public class ParserHelper {
}
}
}
+
+ @Override
+ public void postVisit(Condition condition) {
+ }
});
return result.isEmpty();
}
@@ -74,6 +78,10 @@ public class ParserHelper {
public void visit(Condition condition) {
result.add(condition.getConditionTypeId());
}
+
+ @Override
+ public void postVisit(Condition condition) {
+ }
});
return result;
}
@@ -96,6 +104,7 @@ public class ParserHelper {
}
}
}
+ visitor.postVisit(rootCondition);
}
public static boolean resolveActionTypes(DefinitionsService definitionsService, Rule rule) {
@@ -145,5 +154,49 @@ public class ParserHelper {
interface ConditionVisitor {
void visit(Condition condition);
+ void postVisit(Condition condition);
+ }
+
+ public static Set<String> resolveConditionEventTypes(Condition rootCondition) {
+ if (rootCondition == null) {
+ return new HashSet<>();
+ }
+ EventTypeConditionVisitor eventTypeConditionVisitor = new EventTypeConditionVisitor();
+ visitConditions(rootCondition, eventTypeConditionVisitor);
+ return eventTypeConditionVisitor.getEventTypeIds();
+ }
+
+ static class EventTypeConditionVisitor implements ConditionVisitor {
+
+ private Set<String> eventTypeIds = new HashSet<>();
+ private Stack<String> conditionTypeStack = new Stack<>();
+
+ public void visit(Condition condition) {
+ conditionTypeStack.push(condition.getConditionTypeId());
+ if ("eventTypeCondition".equals(condition.getConditionTypeId())) {
+ String eventTypeId = (String) condition.getParameter("eventTypeId");
+ if (eventTypeId == null) {
+ logger.warn("Null eventTypeId found!");
+ } else {
+ // we must now check the stack to see how many notConditions we have in the parent stack
+ if (conditionTypeStack.contains("notCondition")) {
+ logger.warn("Found complex negative event type condition, will always evaluate rule");
+ eventTypeIds.add("*");
+ } else {
+ eventTypeIds.add(eventTypeId);
+ }
+ }
+ } else if (condition.getConditionType().getParentCondition() != null) {
+ visitConditions(condition.getConditionType().getParentCondition(), this);
+ }
+ }
+
+ public void postVisit(Condition condition) {
+ conditionTypeStack.pop();
+ }
+
+ public Set<String> getEventTypeIds() {
+ return eventTypeIds;
+ }
}
}
diff --git a/services/src/main/java/org/apache/unomi/services/impl/rules/RulesServiceImpl.java b/services/src/main/java/org/apache/unomi/services/impl/rules/RulesServiceImpl.java
index 42e0210..0a46564 100644
--- a/services/src/main/java/org/apache/unomi/services/impl/rules/RulesServiceImpl.java
+++ b/services/src/main/java/org/apache/unomi/services/impl/rules/RulesServiceImpl.java
@@ -64,6 +64,9 @@ public class RulesServiceImpl implements RulesService, EventListenerService, Syn
private List<RuleListenerService> ruleListeners = new CopyOnWriteArrayList<RuleListenerService>();
+ private Map<String,Set<Rule>> rulesByEventType = new HashMap<>();
+ private Boolean optimizedRulesActivated = true;
+
public void setBundleContext(BundleContext bundleContext) {
this.bundleContext = bundleContext;
}
@@ -96,6 +99,10 @@ public class RulesServiceImpl implements RulesService, EventListenerService, Syn
this.rulesStatisticsRefreshInterval = rulesStatisticsRefreshInterval;
}
+ public void setOptimizedRulesActivated(Boolean optimizedRulesActivated) {
+ this.optimizedRulesActivated = optimizedRulesActivated;
+ }
+
public void postConstruct() {
logger.debug("postConstruct {" + bundleContext.getBundle() + "}");
@@ -162,9 +169,23 @@ public class RulesServiceImpl implements RulesService, EventListenerService, Syn
Boolean hasEventAlreadyBeenRaisedForSession = null;
Boolean hasEventAlreadyBeenRaisedForProfile = null;
- List<Rule> allItems = allRules;
+ Set<Rule> eventTypeRules = new HashSet<>(allRules); // local copy to avoid concurrency issues
+ if (optimizedRulesActivated) {
+ eventTypeRules = rulesByEventType.get(event.getEventType());
+ if (eventTypeRules == null) {
+ eventTypeRules = new HashSet<>();
+ }
+ eventTypeRules = new HashSet<>(eventTypeRules); // local copy to avoid concurrency issues
+ Set<Rule> allEventRules = rulesByEventType.get("*");
+ if (allEventRules != null && !allEventRules.isEmpty()) {
+ eventTypeRules.addAll(allEventRules); // retrieve rules that should always be evaluated.
+ }
+ if (eventTypeRules.isEmpty()) {
+ return matchedRules;
+ }
+ }
- for (Rule rule : allItems) {
+ for (Rule rule : eventTypeRules) {
if (!rule.getMetadata().isEnabled()) {
continue;
}
@@ -245,15 +266,34 @@ public class RulesServiceImpl implements RulesService, EventListenerService, Syn
allRuleStatistics.put(ruleStatistics.getItemId(), ruleStatistics);
}
+ public void refreshRules() {
+ try {
+ // we use local variables to make sure we quickly switch the collections since the refresh is called often
+ // we want to avoid concurrency issues with the shared collections
+ List<Rule> newAllRules = getAllRules();
+ this.rulesByEventType = getRulesByEventType(newAllRules);
+ this.allRules = newAllRules;
+ } catch (Throwable t) {
+ logger.error("Error loading rules from persistence back-end", t);
+ }
+ }
+
private List<Rule> getAllRules() {
- List<Rule> allItems = persistenceService.getAllItems(Rule.class, 0, -1, "priority").getList();
- for (Rule rule : allItems) {
+ List<Rule> rules = persistenceService.getAllItems(Rule.class, 0, -1, "priority").getList();
+ for (Rule rule : rules) {
ParserHelper.resolveConditionType(definitionsService, rule.getCondition(), "rule " + rule.getItemId());
ParserHelper.resolveActionTypes(definitionsService, rule);
}
- return allItems;
+ return rules;
}
+ private Map<String,Set<Rule>> getRulesByEventType(List<Rule> rules) {
+ Map<String,Set<Rule>> newRulesByEventType = new HashMap<>();
+ for (Rule rule : rules) {
+ updateRulesByEventType(newRulesByEventType, rule);
+ }
+ return newRulesByEventType;
+ }
public boolean canHandle(Event event) {
return true;
@@ -390,11 +430,7 @@ public class RulesServiceImpl implements RulesService, EventListenerService, Syn
TimerTask task = new TimerTask() {
@Override
public void run() {
- try {
- allRules = getAllRules();
- } catch (Throwable t) {
- logger.error("Error loading rules from persistence back-end", t);
- }
+ refreshRules();
}
};
schedulerService.getScheduleExecutorService().scheduleWithFixedDelay(task, 0,rulesRefreshInterval, TimeUnit.MILLISECONDS);
@@ -512,4 +548,16 @@ public class RulesServiceImpl implements RulesService, EventListenerService, Syn
}
}
+ private void updateRulesByEventType(Map<String,Set<Rule>> rulesByEventType, Rule rule) {
+ Set<String> eventTypeIds = ParserHelper.resolveConditionEventTypes(rule.getCondition());
+ for (String eventTypeId : eventTypeIds) {
+ Set<Rule> rules = rulesByEventType.get(eventTypeId);
+ if (rules == null) {
+ rules = new HashSet<>();
+ }
+ rules.add(rule);
+ rulesByEventType.put(eventTypeId, rules);
+ }
+ }
+
}
diff --git a/services/src/main/resources/OSGI-INF/blueprint/blueprint.xml b/services/src/main/resources/OSGI-INF/blueprint/blueprint.xml
index 5a58f9b..1a888d9 100644
--- a/services/src/main/resources/OSGI-INF/blueprint/blueprint.xml
+++ b/services/src/main/resources/OSGI-INF/blueprint/blueprint.xml
@@ -42,6 +42,7 @@
<cm:property name="segment.daily.dateexpr.evaluation.hourutc" value="5"/>
<cm:property name="rules.refresh.interval" value="1000"/>
<cm:property name="rules.statistics.refresh.interval" value="10000"/>
+ <cm:property name="rules.optimizationActivated" value="true"/>
</cm:default-properties>
</cm:property-placeholder>
@@ -156,6 +157,7 @@
<property name="schedulerService" ref="schedulerServiceImpl"/>
<property name="rulesRefreshInterval" value="${services.rules.refresh.interval}"/>
<property name="rulesStatisticsRefreshInterval" value="${services.rules.statistics.refresh.interval}"/>
+ <property name="optimizedRulesActivated" value="${services.rules.optimizationActivated}"/>
</bean>
<service id="rulesService" ref="rulesServiceImpl">
<interfaces>
diff --git a/services/src/main/resources/org.apache.unomi.services.cfg b/services/src/main/resources/org.apache.unomi.services.cfg
index b54ca81..18875c7 100644
--- a/services/src/main/resources/org.apache.unomi.services.cfg
+++ b/services/src/main/resources/org.apache.unomi.services.cfg
@@ -66,3 +66,8 @@ rules.refresh.interval=${org.apache.unomi.rules.refresh.interval:-1000}
# The interval in milliseconds to use to reload the rules statistics
rules.statistics.refresh.interval=${org.apache.unomi.rules.statistics.refresh.interval:-10000}
+
+# If this setting is active, the rules engine will try to classify the events by event type internally which makes
+# rules execution a lot faster. If there are any problems detected with rules execution, you might want to try to turn
+# off the optimization and file a bug report if this fixed the problem.
+rules.optimizationActivated=${org.apache.unomi.rules.optimizationActivated:-true}
\ No newline at end of file