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