commit 0ba4d136b1401614e85cdc5339037a2d4ef39b5c
Author: Wolfgang Ziegler // fago <fago@wolfgangziegler.net>
Date:   Fri Jan 15 18:05:09 2016 +0100

    Issue #2650886 by fago, das-peter, joelpittet: Build and fetch event and component caches per event or component to reduce locks.

diff --git a/includes/rules.core.inc b/includes/rules.core.inc
index 04b174b..a763d27 100644
--- a/includes/rules.core.inc
+++ b/includes/rules.core.inc
@@ -28,6 +28,21 @@ class RulesEntityController extends EntityAPIControllerExportable {
   }
 
   /**
+   * Fetches all events which have configured, active rules attached.
+   *
+   * @return string[]
+   *   A numerically indexed array of event names.
+   */
+  public function fetchAllConfiguredEvents() {
+    $query = db_select('rules_trigger', 'rt')
+      ->distinct(TRUE)
+      ->fields('rt', array('event'));
+    $query->join('rules_config', 'rc', 'rt.id = rc.id');
+    $query->condition('rc.active', 1);
+    return $query->execute()->fetchCol();
+  }
+
+  /**
    * Overridden.
    * @see DrupalDefaultEntityController::attachLoad()
    */
@@ -1113,21 +1128,25 @@ abstract class RulesPlugin extends RulesExtendable {
       else {
         $plugin_info = $this->pluginInfo();
         if (!empty($plugin_info['component'])) {
-          // When component variables changes rebuild the complete cache so the
+          // When component variables changes rebuild the data cache so the
           // changes to the provided action/condition take affect.
           if (empty($this->original) || $this->componentVariables() != $this->original->componentVariables()) {
-            rules_clear_cache();
+            _rules_clear_data_cache();
           }
-          // Clear components cached for evaluation.
-          cache_clear_all('comp_', 'cache_rules', TRUE);
+          _rules_rebuild_component_cache($this->name);
         }
         elseif ($this->plugin == 'reaction rule') {
-          // Clear event sets cached for evaluation.
-          cache_clear_all('event_', 'cache_rules', TRUE);
-          // Clear event whitelist for rebuild.
-          cache_clear_all('rules_event_whitelist', 'cache_rules', TRUE);
+          // Clear caches for all events involved.
+          $events_to_clear = $this->events();
+          // Make sure any remove event is updated too.
+          if (!empty($original)) {
+            $events_to_clear += array_diff($original->events, $events_to_clear);
+          }
+          foreach ($events_to_clear as $event) {
+            rules_clear_cache_entry('event_' . $event);
+          }
+          rules_clear_cache_entry('rules_event_whitelist');
         }
-        drupal_static_reset('rules_get_cache');
         drupal_static_reset('rules_config_update_dirty_flag');
       }
 
diff --git a/includes/rules.plugins.inc b/includes/rules.plugins.inc
index 90b306d..f1e64b1 100644
--- a/includes/rules.plugins.inc
+++ b/includes/rules.plugins.inc
@@ -753,63 +753,82 @@ class RulesEventSet extends RulesRuleSet {
   /**
    * Cache event-sets per event to allow efficient usage via rules_invoke_event().
    *
+   * @param string|null $event_name
+   *   (optional) The event for which to rebuild the cache. If left empty, the
+   *   cache for all events is rebuilt. However, doing so is discouraged -
+   *   instead just clear caches and let the event cache build on demand (per
+   *   event).
+   *
    * @see rules_get_cache()
    * @see rules_invoke_event()
    */
-  public static function rebuildEventCache() {
+  public static function rebuildEventCache($event_name = NULL) {
+    // If no event is given, rebuild the cache for all known events.
+    if (!isset($event_name)) {
+      foreach (rules_get_cache('rules_event_whitelist') as $event_name => $value) {
+        self::rebuildEventCache($event_name);
+      }
+      return;
+    }
+
     // Set up the per-event cache.
     $events = rules_fetch_data('event_info');
-    $sets = array();
+    $event_base_name = rules_get_event_base_name($event_name);
+
+    // Skip not defined events.
+    if (empty($events[$event_base_name])) {
+      return;
+    }
+
     // Add all rules associated with this event to an EventSet for caching.
-    $rules = rules_config_load_multiple(FALSE, array('plugin' => 'reaction rule', 'active' => TRUE));
+    $rules = rules_config_load_multiple(FALSE, array(
+      'plugin' => 'reaction rule',
+      'active' => TRUE,
+      'event' => $event_name
+    ));
 
-    foreach ($rules as $name => $rule) {
-      foreach ($rule->events() as $event_name) {
-        $event_base_name = rules_get_event_base_name($event_name);
-        // Skip not defined events.
-        if (empty($events[$event_base_name])) {
-          continue;
-        }
-        // Create an event set if not yet done.
-        if (!isset($sets[$event_name])) {
-          $handler = rules_get_event_handler($event_name, $rule->getEventSettings($event_name));
+    // No rules, looks like there is a wrong event whitelist. Clear it so it
+    // gets rebuilt on the next request.
+    if (!$rules) {
+      rules_clear_cache_entry('rules_event_whitelist');
+      return;
+    }
 
-          // Start the event dispatcher for this event, if any.
-          if ($handler instanceof RulesEventDispatcherInterface && !$handler->isWatching()) {
-            $handler->startWatching();
-          }
+    foreach ($rules as $name => $rule) {
+      // Create an event set if not yet done.
+      if (!isset($event_set)) {
+        $handler = rules_get_event_handler($event_name, $rule->getEventSettings($event_name));
 
-          // Update the event info with the variables available based on the
-          // event settings.
-          $event_info = $events[$event_base_name];
-          $event_info['variables'] = $handler->availableVariables();
-          $sets[$event_name] = new RulesEventSet($event_info);
-          $sets[$event_name]->name = $event_name;
+        // Start the event dispatcher for this event, if any.
+        if ($handler instanceof RulesEventDispatcherInterface && !$handler->isWatching()) {
+          $handler->startWatching();
         }
 
-        // If a rule is marked as dirty, check if this still applies.
-        if ($rule->dirty) {
-          rules_config_update_dirty_flag($rule);
-        }
-        if (!$rule->dirty) {
-          // Clone the rule to avoid modules getting the changed version from
-          // the static cache.
-          $sets[$event_name]->rule(clone $rule);
-        }
+        // Update the event info with the variables available based on the
+        // event settings.
+        $event_info = $events[$event_base_name];
+        $event_info['variables'] = $handler->availableVariables();
+        $event_set = new RulesEventSet($event_info);
+        $event_set->name = $event_name;
+      }
+
+      // If a rule is marked as dirty, check if this still applies.
+      if ($rule->dirty) {
+        rules_config_update_dirty_flag($rule);
+      }
+      if (!$rule->dirty) {
+        // Clone the rule to avoid modules getting the changed version from
+        // the static cache.
+        $event_set->rule(clone $rule);
       }
     }
 
     // Create cache items for all created sets.
-    foreach ($sets as $event_name => $set) {
-      $set->sortChildren();
-      $set->optimize();
-      // Allow modules to alter the cached event set.
-      drupal_alter('rules_event_set', $event_name, $set);
-      rules_set_cache('event_' . $event_name, $set);
-    }
-    // Cache a whitelist of configured events so we can use it to speed up later
-    // calls. See rules_invoke_event().
-    rules_set_cache('rules_event_whitelist', array_flip(array_keys($sets)));
+    $event_set->sortChildren();
+    $event_set->optimize();
+    // Allow modules to alter the cached event set.
+    drupal_alter('rules_event_set', $event_name, $event_set);
+    rules_set_cache('event_' . $event_name, $event_set);
   }
 
   protected function stateVariables($element = NULL) {
diff --git a/rules.module b/rules.module
index e148163..2d11f4c 100644
--- a/rules.module
+++ b/rules.module
@@ -383,11 +383,17 @@ function &rules_get_cache($cid = 'data') {
       }
       elseif (strpos($cid, 'comp_') === 0) {
         $cache[$cid] = FALSE;
-        _rules_rebuild_component_cache();
+        // Cut of the leading 'comp_' prefix to determine the event name.
+        _rules_rebuild_component_cache(substr($cid, 5));
       }
-      elseif (strpos($cid, 'event_') === 0 || $cid == 'rules_event_whitelist') {
+      elseif ($cid == 'rules_event_whitelist') {
         $cache[$cid] = FALSE;
-        RulesEventSet::rebuildEventCache();
+        _rules_rebuild_event_whitelist_cache();
+      }
+      elseif (strpos($cid, 'event_') === 0) {
+        $cache[$cid] = FALSE;
+        // Cut of the leading 'event_' prefix to determine the event name.
+        RulesEventSet::rebuildEventCache(substr($cid, 6));
       }
       else {
         $cache[$cid] = FALSE;
@@ -427,13 +433,23 @@ function _rules_rebuild_cache(&$cache) {
 }
 
 /**
- * Cache components to allow efficient usage via rules_invoke_component().
+ * Rebuilds the cache for a component.
+ *
+ * Components are cached to allow efficient usage via rules_invoke_component().
+ *
+ * @param string|null $component_name
+ *   (optional) If set, the name of the component for which the cache is to be
+ *   rebuilt. If left empty, the cache for all components is rebuilt.
  *
  * @see rules_invoke_component()
  * @see rules_get_cache()
  */
-function _rules_rebuild_component_cache() {
-  $components = rules_get_components();
+function _rules_rebuild_component_cache($component_name = NULL) {
+  $conditions = array();
+  if (!empty($component_name)) {
+    $conditions['name'] = $component_name;
+  }
+  $components = rules_get_components(FALSE, NULL, $conditions);
 
   foreach ($components as $id => $component) {
     // If a component is marked as dirty, check if this still applies.
@@ -453,6 +469,17 @@ function _rules_rebuild_component_cache() {
 }
 
 /**
+ * Rebuilds the rules event whitelist cache.
+ */
+function _rules_rebuild_event_whitelist_cache() {
+  // Cache a whitelist of configured events so we can use it to speed up later
+  // calls. See rules_invoke_event().
+  $events = entity_get_controller('rules_config')->fetchAllConfiguredEvents();
+  $whitelist = array_flip($events);
+  rules_set_cache('rules_event_whitelist', $whitelist);
+}
+
+/**
  * Sets a rules cache item.
  *
  * In addition to calling cache_set(), this function makes sure the cache item
@@ -487,6 +514,28 @@ function rules_clear_cache() {
 }
 
 /**
+ * Clears a specific cache entry only.
+ *
+ * @param $cid
+ *   The cache id of the cache entry to clear.
+ */
+function rules_clear_cache_entry($cid) {
+  $cache = &drupal_static('rules_get_cache', array());
+  unset($cache[$cid]);
+  cache_clear_all($cid, 'cache_rules');
+}
+
+/**
+ * Clears the rules 'data' cache only.
+ */
+function _rules_clear_data_cache() {
+  drupal_static_reset('rules_get_cache');
+  drupal_static_reset('rules_fetch_data');
+  drupal_static_reset('rules_config_update_dirty_flag');
+  cache_clear_all('data:', 'cache_rules', TRUE);
+}
+
+/**
  * Imports the given export and returns the imported configuration.
  *
  * @param $export
